diff --git a/.github/workflows/dep-lic-scan.yaml b/.github/workflows/dep-lic-scan.yaml deleted file mode 100644 index afb197bf137..00000000000 --- a/.github/workflows/dep-lic-scan.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: Dependency and License Scan -on: - push: - branches: - - '4.x' - - '3.x' - paths-ignore: - - 'manual/**' - - 'faq/**' - - 'upgrade_guide/**' - - 'changelog/**' -jobs: - scan-repo: - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v2 - - name: Install Fossa CLI - run: | - curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install-latest.sh | bash -s -- -b . - - name: Scan for dependencies and licenses - run: | - FOSSA_API_KEY=${{ secrets.FOSSA_PUSH_ONLY_API_KEY }} ./fossa analyze diff --git a/.gitignore b/.gitignore index 7cf0248307f..eaf1a9ef8b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,15 @@ -target/ -cobertura-history/ -testing/ .settings -.classpath -.project -doc -docs -notes .DS_Store +.documenter_local_last_run /.idea *.iml +.classpath +.project -/driver-core/dependency-reduced-pom.xml .java-version +.documenter_local_last_run +/docs +target/ +dependency-reduced-pom.xml diff --git a/.travis.yml b/.travis.yml index c63c5b781f2..a7f970a8c20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,20 @@ language: java -dist: trusty -jdk: -- openjdk7 -- oraclejdk8 sudo: false -before_install: - # Require JDK8 for compiling - - jdk_switcher use oraclejdk8 -before_script: - # Switch back to configured JDK for running tests - - jdk_switcher use $TRAVIS_JDK_VERSION +# see https://sormuras.github.io/blog/2018-03-20-jdk-matrix.html +matrix: + include: + # 8 + - env: JDK='OpenJDK 8' + jdk: openjdk8 + # 11 + - env: JDK='OpenJDK 11' + # switch to JDK 11 before running tests + before_script: . $TRAVIS_BUILD_DIR/ci/install-jdk.sh -F 11 -L GPL +before_install: + # Require JDK8 for compiling + - jdk_switcher use openjdk8 +install: mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V +script: mvn test -Djacoco.skip=true -B -V cache: directories: - - $HOME/.m2 -notifications: - slack: - secure: PxE3bfSyGGRkvHqlR5HciuqAWO7qxJk/8/j0odRJ4FxoT8QYzlGjt8dxN54f+QmKEjFv9W3Gn8ufZmk5lOZlS3L5vWd3MofAfSUzuDpDY9v95UEwltdmedDLjwkdIGnhpYAI7VvLRrhny8sQfx3uSncCpGiLpDqubZRF4D7IDxQ= + - $HOME/.m2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5450ce3cd4..d8d7a4c778a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ We follow the [Google Java Style Guide](https://google.github.io/styleguide/java https://github.com/google/google-java-format for IDE plugins. The rules are not configurable. The build will fail if the code is not formatted. To format all files from the command line, run: - + ``` mvn fmt:format ``` @@ -15,97 +15,429 @@ Some aspects are not covered by the formatter: * braces must be used with `if`, `else`, `for`, `do` and `while` statements, even when the body is empty or contains only a single statement. +* XML files: indent with two spaces and wrap to respect the column limit of 100 characters. + +## Coding style -- production code + +Do not use static imports. They make things harder to understand when you look at the code +someplace where you don't have IDE support, like Github's code view. -Also, if your IDE sorts import statements automatically, make sure it follows the same order as the -formatter: all static imports in ASCII sort order, followed by a blank line, followed by all regular -imports in ASCII sort order. In addition, please avoid using wildcard imports. +Avoid abbreviations in class and variable names. A good rule of thumb is that you should only use +them if you would also do so verbally, for example "id" and "config" are probably reasonable. +Single-letter variables are permissible if the variable scope is only a few lines, or for commonly +understood cases (like `i` for a loop index). -## Working on an issue +Keep source files short. Short files are easy to understand and test. The average should probably +be around 200-300 lines. -Before starting to work on something, please comment in JIRA or ask on the mailing list -to make sure nobody else is working on it. +### Javadoc -If your fix applies to multiple branches, base your work on the lowest active branch. Since version 3 of the driver, -we've adopted [semantic versioning](http://semver.org/) and our branches use the following scheme: +All types in "API" packages must be documented. For "internal" packages, documentation is optional, +but in no way discouraged: it's generally a good idea to have a class-level comment that explains +where the component fits in the architecture, and anything else that you feel is important. +You don't need to document every parameter or return type, or even every method. Don't document +something if it is completely obvious, we don't want to end up with this: + +```java +/** + * Returns the name. + * + * @return the name + */ +String getName(); ``` - 3.0.1 3.0.2 ... 3.1.1 ... - -----*----------*------> 3.0.x -----*------> 3.1.x - / / - / / - / / ------*-----------------------------------*-------------------------> 3.x - 3.0.0 3.1.0 ... -Legend: - > branch - * tag +On the other hand, there is often something useful to say about a method, so most should have at +least a one-line comment. Use common sense. + +Driver users coding in their IDE should find the right documentation at the right time. Try to +think of how they will come into contact with the class. For example, if a type is constructed with +a builder, each builder method should probably explain what the default is when you don't call it. + +Avoid using too many links, they can make comments harder to read, especially in the IDE. Link to a +type the first time it's mentioned, then use a text description ("this registry"...) or an `@code` +block. Don't link to a class in its own documentation. Don't link to types that appear right below +in the documented item's signature. + +```java +/** +* @return this {@link Builder} <-- completely unnecessary +*/ +Builder withLimit(int limit) { +``` + +### Logs + +We use SLF4J; loggers are declared like this: + +```java +private static final Logger LOG = LoggerFactory.getLogger(TheEnclosingClass.class); ``` -- new features are developed on "minor" branches such as `3.x`, where minor releases (ending in `.0`) happen. -- bugfixes go to "patch" branches such as `3.0.x` and `3.1.x`, where patch releases (ending in `.1`, `.2`...) happen. -- patch branches are regularly merged to the right (`3.0.x` to `3.1.x`) and to the bottom (`3.1.x` to `3.x`) so that - bugfixes are applied to newer versions too. +Logs are intended for two personae: + +* Ops who manage the application in production. +* Developers (maybe you) who debug a particular issue. + +The first 3 log levels are for ops: + +* `ERROR`: something that renders the driver -- or a part of it -- completely unusable. An action is + required to fix it: bouncing the client, applying a patch, etc. +* `WARN`: something that the driver can recover from automatically, but indicates a configuration or + programming error that should be addressed. For example: the driver connected successfully, but + one of the contact points in the configuration was malformed; the same prepared statement is being + prepared multiple time by the application code. +* `INFO`: something that is part of the normal operation of the driver, but might be useful to know + for an operator. For example: the driver has initialized successfully and is ready to process + queries; an optional dependency was detected in the classpath and activated an enhanced feature. + +Do not log errors that are rethrown to the client (such as the error that you're going to complete a +request with). This is annoying for ops because they see a lot of stack traces that require no +actual action on their part, because they're already handled by application code. + +Similarly, do not log stack traces for non-critical errors. If you still want the option to get the +trace for debugging, see the `Loggers.warnWithException` utility. + +The last 2 levels are for developers, to help follow what the driver is doing from a "black box" +perspective (think about debugging an issue remotely, and all you have are the logs). + +* `TRACE`: anything that happens **for every user request**. Not only request handling, but all + related components (e.g. timestamp generators, policies, etc). +* `DEBUG`: everything else. For example, node state changes, control connection activity, etc. + +Note that `DEBUG` and `TRACE` can coexist within the same component, for example the LBP +initializing is a one-time event, but returning a query plan is a per-request event. + +Logs statements start with a prefix that identifies its origin, for example: + +* for components that are unique to the cluster instance, just the cluster name: `[c0]`. +* for sessions, the cluster name + a generated unique identifier: `[c0|s0]`. +* for channel pools, the session identifier + the address of the node: `[c0|s0|/127.0.0.2:9042]`. +* for channels, the identifier of the owner (session or control connection) + the Netty identifier, + which indicates the local and remote ports: + `[c0|s0|id: 0xf9ef0b15, L:/127.0.0.1:51482 - R:/127.0.0.1:9042]`. +* for request handlers, the session identifier, a unique identifier, and the index of the + speculative execution: `[c0|s0|1077199500|0]`. + +Tests run with the configuration defined in `src/test/resources/logback-test.xml`. The default level +for driver classes is `WARN`, but you can override it with a system property: `-DdriverLevel=DEBUG`. +A nice setup is to use `DEBUG` when you run from your IDE, and keep the default for the command +line. + +When you add or review new code, take a moment to run the tests in `DEBUG` mode and check if the +output looks good. + +### No stream API + +Please don't use `java.util.stream` in the driver codebase. Streams were designed for *data +processing*, not to make your collection traversals "functional". + +Here's an example from the driver codebase (`ChannelSet`): + +```java +DriverChannel[] snapshot = this.channels; +DriverChannel best = null; +int bestScore = 0; +for (DriverChannel channel : snapshot) { + int score = channel.availableIds(); + if (score > bestScore) { + bestScore = score; + best = channel; + } +} +return best; +``` + +And here's a terrible way to rewrite it using streams: + +```java +// Don't do this: +DriverChannel best = + Stream.of(snapshot) + .reduce((a, b) -> a.availableIds() > b.availableIds() ? a : b) + .get(); +``` + +The stream version is not easier to read, and will probably be slower (creating intermediary objects +vs. an array iteration, compounded by the fact that this particular array typically has a low +cardinality). + +The driver never does the kind of processing that the stream API is intended for; the only large +collections we manipulate are result sets, and these get passed on to the client directly. + +### Never assume a specific format for `toString()` + +Only use `toString()` for debug logs or exception messages, and always assume that its format is +unspecified and can change at any time. + +If you need a specific string representation for a class, make it a dedicated method with a +documented format, for example `toCqlLiteral`. Otherwise it's too easy to lose track of the intended +usage and break things: for example, someone modifies your `toString()` method to make their logs +prettier, but unintentionally breaks the script export feature that expected it to produce CQL +literals. + +`toString()` can delegate to `toCqlLiteral()` if that is appropriate for logs. + + +### Concurrency annotations + +We use the [JCIP annotations](http://jcip.net/annotations/doc/index.html) to document thread-safety +policies. + +Add them for all new code, with the exception of: + +* enums and interfaces; +* utility classes (only static methods); +* test code. + +Make sure you import the types from `net.jcip`, there are homonyms in the classpath. + + +### Nullability annotations + +We use the [Spotbugs annotations](https://spotbugs.github.io) to document nullability of parameters, +method return types and class members. + +Please annotate any new class or interface with the appropriate annotations: `@NonNull`, `@Nullable`. Make sure you import +the types from `edu.umd.cs.findbugs.annotations`, there are homonyms in the classpath. + + +## Coding style -- test code + +Static imports are permitted in a couple of places: +* All AssertJ methods, e.g.: + ```java + assertThat(node.getDatacenter()).isNotNull(); + fail("Expecting IllegalStateException to be thrown"); + ``` +* All Mockito methods, e.g.: + ```java + when(codecRegistry.codecFor(DataTypes.INT)).thenReturn(codec); + verify(codec).decodePrimitive(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + ``` + +Test methods names use lower snake case, generally start with `should`, and clearly indicate the +purpose of the test, for example: `should_fail_if_key_already_exists`. If you have trouble coming +up with a simple name, it might be a sign that your test does too much, and should be split. + +We use AssertJ (`assertThat`) for assertions. Don't use JUnit assertions (`assertEquals`, +`assertNull`, etc). + +Don't try to generify at all cost: a bit of duplication is acceptable, if that helps keep the tests +simple to understand (a newcomer should be able to understand how to fix a failing test without +having to read too much code). + +Test classes can be a bit longer, since they often enumerate similar test cases. You can also +factor some common code in a parent abstract class named with "XxxTestBase", and then split +different families of tests into separate child classes. For example, `CqlRequestHandlerTestBase`, +`CqlRequestHandlerRetryTest`, `CqlRequestHandlerSpeculativeExecutionTest`... + +### Unit tests + +They live in the same module as the code they are testing. They should be fast and not start any +external process. They usually target one specific component and mock the rest of the driver +context. + +### Integration tests + +They live in the `integration-tests` module, and exercise the whole driver stack against an external +process, which can be either one of: +* [Simulacron](https://github.com/datastax/simulacron): simulates Cassandra nodes on loopback + addresses; your test must "prime" data, i.e. tell the nodes what results to return for + pre-determined queries. + + For an example of a Simulacron-based test, see `NodeTargetingIT`. +* [CCM](https://github.com/pcmanus/ccm): launches actual Cassandra nodes locally. The `ccm` + executable must be in the path. + + You can pass a `-Dccm.version` system property to the build to target a particular Cassandra + version (it defaults to 3.11.0). `-Dccm.directory` allows you to point to a local installation + -- this can be a checkout of the Cassandra codebase, as long as it's built. See `CcmBridge` in + the driver codebase for more details. + + For an example of a CCM-based test, see `PlainTextAuthProviderIT`. + +Integration tests are divided into three categories: -The current active versions are 3.0 and 3.1. Therefore: +#### Parallelizable tests -- if you're fixing a bug on a feature that existed since 3.0, target `3.0.x`. Your changes will be available in future - 3.0 and 3.1 patch versions. -- if you're fixing a bug on a 3.1-only feature, target `3.1.x`. Your changes will be available in a future 3.1 patch - version. -- if you're adding a new feature, target `3.x`. Your changes will be available in the upcoming 3.2.0. +These tests can be run in parallel, to speed up the build. They either use: +* dedicated Simulacron instances. These are lightweight, and Simulacron will manage the ports to + make sure that there are no collisions. +* a shared, one-node CCM cluster. Each test works in its own keyspace. -Before you send your pull request, make sure that: +The build runs them with a configurable degree of parallelism (currently 8). The shared CCM cluster +is initialized the first time it's used, and stopped before moving on to serial tests. -- you have a unit test that failed before the fix and succeeds after. -- the fix is mentioned in `changelog/README.md`. -- the commit message include the reference of the JIRA ticket for automatic linking - (example: `JAVA-503: Fix NPE when a connection fails during pool construction.`). +To make an integration test parallelizable, annotate it with `@Category(ParallelizableTests.class)`. +If you use CCM, it **must** be with `CcmRule`. -As long as your pull request is not merged, it's OK to rebase your branch and push with -`--force`. +For an example of a Simulacron-based parallelizable test, see `NodeTargetingIT`. For a CCM-based +test, see `DirectCompressionIT`. -If you want to contribute but don't have a specific issue in mind, the [lhf](https://datastax-oss.atlassian.net/secure/IssueNavigator.jspa?reset=true&mode=hide&jqlQuery=project%20%3D%20JAVA%20AND%20status%20in%20(Open%2C%20Reopened)%20AND%20labels%20%3D%20lhf) -label in JIRA is a good place to start: it marks "low hanging fruits" that don't require -in-depth knowledge of the codebase. +#### Serial tests -## Editor configuration +These tests cannot run in parallel, in general because they require CCM clusters of different sizes, +or with a specific configuration (we never run more than one CCM cluster simultaneously: it would be +too resource-intensive, and too complicated to manage all the ports). -We use IntelliJ IDEA with the default formatting options, with one exception: check -"Enable formatter markers in comments" in Preferences > Editor > Code Style. +The build runs them one by one, after the parallelizable tests. + +To make an integration test serial, do not annotate it with `@Category`. The CCM rule **must** be +`CustomCcmRule`. + +For an example, see `DefaultLoadBalancingPolicyIT`. + +Note: if multiple serial tests have a common "base" class, do not pull up `CustomCcmRule`, each +child class must have its own instance. Otherwise they share the same CCM instance, and the first +one destroys it on teardown. See `TokenITBase` for how to organize code in those cases. + +#### Isolated tests + +Not only can those tests not run in parallel, they also require specific environment tweaks, +typically system properties that need to be set before initialization. + +The build runs them one by one, *each in its own JVM fork*, after the serial tests. + +To isolate an integration test, annotate it with `@Category(IsolatedTests.class)`. The CCM rule +**must** be `CustomCcmRule`. + +For an example, see `HeapCompressionIT`. -Please format your code and organize imports before submitting your changes. ## Running the tests -We use TestNG. There are 3 test categories: +### Unit tests + + mvn clean test + +This currently takes about 30 seconds. The goal is to keep it within a couple of minutes (it runs +for each commit if you enable the pre-commit hook -- see below). + +### Integration tests + + mvn clean verify + +This currently takes about 9 minutes. We don't have a hard limit, but ideally it should stay within +30 minutes to 1 hour. + +You can skip test categories individually with `-DskipParallelizableITs`, `-DskipSerialITs` and +`-DskipIsolatedITs`. + +### Configuring MacOS for Simulacron + +Simulacron (used in integration tests) relies on loopback aliases to simulate multiple nodes. On +Linux or Windows, you shouldn't have anything to do. On MacOS, run this script: + +``` +#!/bin/bash +for sub in {0..4}; do + echo "Opening for 127.0.$sub" + for i in {0..255}; do sudo ifconfig lo0 alias 127.0.$sub.$i up; done +done +``` + +Note that this is known to cause temporary increased CPU usage in OS X initially while mDNSResponder +acclimates itself to the presence of added IP addresses. This lasts several minutes. Also, this does +not survive reboots. + + +## License headers -- "unit": pure Java unit tests. -- "short" and "long": integration tests that launch Cassandra instances. +The build will fail if some license headers are missing. To update all files from the command line, +run: -The Maven build uses profiles named after the categories to choose which tests to run: +``` +mvn license:format +``` + +## Pre-commit hook (highly recommended) + +Ensure `pre-commit.sh` is executable, then run: ``` -mvn test -Pshort +ln -s ../../pre-commit.sh .git/hooks/pre-commit +``` + +This will only allow commits if the tests pass. It is also a good reminder to keep the test suite +short. + +Note: the tests run on the current state of the working directory. I tried to add a `git stash` in +the script to only test what's actually being committed, but I couldn't get it to run reliably +(it's still in there but commented). Keep this in mind when you commit, and don't forget to re-add +the changes if the first attempt failed and you fixed the tests. + +## Commits + +Keep your changes **focused**. Each commit should have a single, clear purpose expressed in its +message. + +Resist the urge to "fix" cosmetic issues (add/remove blank lines, move methods, etc.) in existing +code. This adds cognitive load for reviewers, who have to figure out which changes are relevant to +the actual issue. If you see legitimate issues, like typos, address them in a separate commit (it's +fine to group multiple typo fixes in a single commit). + +Isolate trivial refactorings into separate commits. For example, a method rename that affects dozens +of call sites can be reviewed in a few seconds, but if it's part of a larger diff it gets mixed up +with more complex changes (that might affect the same lines), and reviewers have to check every +line. + +Commit message subjects start with a capital letter, use the imperative form and do **not** end +with a period: + +* correct: "Add test for CQL request handler" +* incorrect: "~~Added test for CQL request handler~~" +* incorrect: "~~New test for CQL request handler~~" + +Avoid catch-all messages like "Minor cleanup", "Various fixes", etc. They don't provide any useful +information to reviewers, and might be a sign that your commit contains unrelated changes. + +We don't enforce a particular subject line length limit, but try to keep it short. + +You can add more details after the subject line, separated by a blank line. The following pattern +(inspired by [Netty](http://netty.io/wiki/writing-a-commit-message.html)) is not mandatory, but +welcome for complex changes: + ``` +One line description of your change + +Motivation: -The default is "unit". Each profile runs the ones before it ("short" runs unit, etc.) +Explain here the context, and why you're making that change. +What is the problem you're trying to solve. + +Modifications: -Integration tests use [CCM](https://github.com/pcmanus/ccm) to bootstrap Cassandra instances. -Two Maven properties control its execution: +Describe the modifications you've done. + +Result: -- `cassandra.version`: the Cassandra version. This has a default value in the root POM, - you can override it on the command line (`-Dcassandra.version=...`). -- `ipprefix`: the prefix of the IP addresses that the Cassandra instances will bind to (see - below). This defaults to `127.0.1.`. +After your change, what will change. +``` + +## Pull requests + +Like commits, pull requests should be focused on a single, clearly stated goal. +Don't base a pull request onto another one, it's too complicated to follow two branches that evolve +at the same time. If a ticket depends on another, wait for the first one to be merged. -CCM launches multiple Cassandra instances on localhost by binding to different addresses. The -driver uses up to 10 different instances (127.0.1.1 to 127.0.1.10 with the default prefix). -You'll need to define loopback aliases for this to work, on Mac OS X your can do it with: +If you have to address feedback, avoid rewriting the history (e.g. squashing or amending commits): +this makes the reviewers' job harder, because they have to re-read the full diff and figure out +where your new changes are. Instead, push a new commit on top of the existing history; it will be +squashed later when the PR gets merged. If the history is complex, it's a good idea to indicate in +the message where the changes should be squashed: ``` -sudo ifconfig lo0 alias 127.0.1.1 up -sudo ifconfig lo0 alias 127.0.1.2 up -... +* 20c88f4 - Address feedback (to squash with "Add metadata parsing logic") (36 minutes ago) +* 7044739 - Fix various typos in Javadocs (2 days ago) +* 574dd08 - Add metadata parsing logic (2 days ago) ``` + +(Note that the message refers to the other commit's subject line, not the SHA-1. This way it's still +relevant if there are intermediary rebases.) + +If you need new stuff from the base branch, it's fine to rebase and force-push, as long as you don't +rewrite the history. Just give a heads up to the reviewers beforehand. Don't push a merge commit to +a pull request. diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 388395b7615..00000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,549 +0,0 @@ -#!groovy - -def initializeEnvironment() { - env.DRIVER_DISPLAY_NAME = 'CassandraⓇ Java Driver 3.x' - env.DRIVER_METRIC_TYPE = 'oss' - if (env.GIT_URL.contains('riptano/java-driver')) { - env.DRIVER_DISPLAY_NAME = 'private ' + env.DRIVER_DISPLAY_NAME - env.DRIVER_METRIC_TYPE = 'oss-private' - } else if (env.GIT_URL.contains('java-dse-driver')) { - env.DRIVER_DISPLAY_NAME = 'DSE Java Driver 1.x' - env.DRIVER_METRIC_TYPE = 'dse' - } - - env.GIT_SHA = "${env.GIT_COMMIT.take(7)}" - env.GITHUB_PROJECT_URL = "https://${GIT_URL.replaceFirst(/(git@|http:\/\/|https:\/\/)/, '').replace(':', '/').replace('.git', '')}" - env.GITHUB_BRANCH_URL = "${GITHUB_PROJECT_URL}/tree/${env.BRANCH_NAME}" - env.GITHUB_COMMIT_URL = "${GITHUB_PROJECT_URL}/commit/${env.GIT_COMMIT}" - - env.MAVEN_HOME = "${env.HOME}/.mvn/apache-maven-3.2.5" - env.PATH = "${env.MAVEN_HOME}/bin:${env.PATH}" - env.JAVA_HOME = sh(label: 'Get JAVA_HOME',script: '''#!/bin/bash -le - . ${JABBA_SHELL} - jabba which ${JABBA_VERSION}''', returnStdout: true).trim() - env.JAVA8_HOME = sh(label: 'Get JAVA8_HOME',script: '''#!/bin/bash -le - . ${JABBA_SHELL} - jabba which 1.8''', returnStdout: true).trim() - - sh label: 'Download Apache CassandraⓇ',script: '''#!/bin/bash -le - . ${JABBA_SHELL} - jabba use ${JABBA_VERSION} - . ${CCM_ENVIRONMENT_SHELL} ${SERVER_VERSION} - ''' - - sh label: 'Display Java and environment information',script: '''#!/bin/bash -le - # Load CCM environment variables - set -o allexport - . ${HOME}/environment.txt - set +o allexport - - . ${JABBA_SHELL} - jabba use ${JABBA_VERSION} - - java -version - mvn -v - printenv | sort - ''' -} - -def buildDriver(jabbaVersion) { - withEnv(["BUILD_JABBA_VERSION=${jabbaVersion}"]) { - sh label: 'Build driver', script: '''#!/bin/bash -le - . ${JABBA_SHELL} - jabba use ${BUILD_JABBA_VERSION} - - mvn -B -V install -DskipTests - ''' - } -} - -def executeTests() { - sh label: 'Execute tests', script: '''#!/bin/bash -le - # Load CCM environment variables - set -o allexport - . ${HOME}/environment.txt - set +o allexport - - . ${JABBA_SHELL} - jabba use ${JABBA_VERSION} - - printenv | sort - - mvn -B -V verify \ - -fail-never -P${TEST_PROFILE} \ - -Dcom.datastax.driver.TEST_BASE_NODE_WAIT=120 \ - -Dcom.datastax.driver.NEW_NODE_DELAY_SECONDS=100 \ - -Dcassandra.version=${CCM_CASSANDRA_VERSION} \ - -Ddse=${CCM_IS_DSE} \ - -Dccm.java.home=${CCM_JAVA_HOME} \ - -Dccm.path=${CCM_JAVA_HOME}/bin \ - -Dccm.maxNumberOfNodes=3 \ - -DfailIfNoTests=false \ - -Dmaven.test.failure.ignore=true \ - -Dmaven.javadoc.skip=true \ - -Dproxy.path=${HOME}/proxy - - # run isolated tests - mvn -B -V verify \ - -fail-never -Pisolated \ - -Dcom.datastax.driver.TEST_BASE_NODE_WAIT=120 \ - -Dcom.datastax.driver.NEW_NODE_DELAY_SECONDS=100 \ - -Dcassandra.version=${CCM_CASSANDRA_VERSION} \ - -Ddse=${CCM_IS_DSE} \ - -Dccm.java.home=${CCM_JAVA_HOME} \ - -Dccm.path=${CCM_JAVA_HOME}/bin \ - -Dccm.maxNumberOfNodes=3 \ - -DfailIfNoTests=false \ - -Dmaven.test.failure.ignore=true \ - -Dmaven.javadoc.skip=true - ''' -} - -def executeCodeCoverage() { - jacoco( - execPattern: '**/target/jacoco.exec', - classPattern: '**/classes', - sourcePattern: '**/src/main/java' - ) -} - -def notifySlack(status = 'started') { - // Notify Slack channel for every build except adhoc executions - if (params.ADHOC_BUILD_TYPE != 'BUILD-AND-EXECUTE-TESTS') { - // Set the global pipeline scoped environment (this is above each matrix) - env.BUILD_STATED_SLACK_NOTIFIED = 'true' - - def buildType = 'Commit' - if (params.CI_SCHEDULE != 'DO-NOT-CHANGE-THIS-SELECTION') { - buildType = "${params.CI_SCHEDULE.toLowerCase().capitalize()}" - } - - def color = 'good' // Green - if (status.equalsIgnoreCase('aborted')) { - color = '808080' // Grey - } else if (status.equalsIgnoreCase('unstable')) { - color = 'warning' // Orange - } else if (status.equalsIgnoreCase('failed')) { - color = 'danger' // Red - } - - def message = """Build ${status} for ${env.DRIVER_DISPLAY_NAME} [${buildType}] -<${env.GITHUB_BRANCH_URL}|${env.BRANCH_NAME}> - <${env.RUN_DISPLAY_URL}|#${env.BUILD_NUMBER}> - <${env.GITHUB_COMMIT_URL}|${env.GIT_SHA}>""" - if (!status.equalsIgnoreCase('Started')) { - message += """ -${status} after ${currentBuild.durationString - ' and counting'}""" - } - - slackSend color: "${color}", - channel: "#java-driver-dev-bots", - message: "${message}" - } -} - -def describePerCommitStage() { - script { - currentBuild.displayName = "Per-Commit build" - currentBuild.description = 'Per-Commit build and testing of development Apache CassandraⓇ against Oracle JDK 8' - } -} - -def describeAdhocAndScheduledTestingStage() { - script { - if (params.CI_SCHEDULE == 'DO-NOT-CHANGE-THIS-SELECTION') { - // Ad-hoc build - currentBuild.displayName = "Adhoc testing" - currentBuild.description = "Testing ${params.ADHOC_BUILD_AND_EXECUTE_TESTS_SERVER_VERSION} against JDK version ${params.ADHOC_BUILD_AND_EXECUTE_TESTS_JABBA_VERSION}" - } else { - // Scheduled build - currentBuild.displayName = "${params.CI_SCHEDULE.toLowerCase().replaceAll('_', ' ').capitalize()} schedule" - currentBuild.description = "Testing server versions [${params.CI_SCHEDULE_SERVER_VERSIONS}] against JDK version ${params.CI_SCHEDULE_JABBA_VERSION}" - } - } -} - -// branch pattern for cron -// should match 3.x, 4.x, 4.5.x, etc -def branchPatternCron() { - ~"((\\d+(\\.[\\dx]+)+))" -} - -pipeline { - agent none - - // Global pipeline timeout - options { - timeout(time: 10, unit: 'HOURS') - buildDiscarder(logRotator(artifactNumToKeepStr: '10', // Keep only the last 10 artifacts - numToKeepStr: '50')) // Keep only the last 50 build records - } - - parameters { - choice( - name: 'ADHOC_BUILD_TYPE', - choices: ['BUILD', 'BUILD-AND-EXECUTE-TESTS'], - description: '''

Perform a adhoc build operation

- - - - - - - - - - - - - - - -
ChoiceDescription
BUILDPerforms a Per-Commit build
BUILD-AND-EXECUTE-TESTSPerforms a build and executes the integration and unit tests
''') - choice( - name: 'ADHOC_BUILD_AND_EXECUTE_TESTS_SERVER_VERSION', - choices: ['2.1', // Legacy Apache CassandraⓇ - '2.2', // Legacy Apache CassandraⓇ - '3.0', // Previous Apache CassandraⓇ - '3.11', // Current Apache CassandraⓇ - '4.0', // Development Apache CassandraⓇ - 'dse-5.1', // Legacy DataStax Enterprise - 'dse-6.0', // Previous DataStax Enterprise - 'dse-6.7', // Previous DataStax Enterprise - 'dse-6.8.0', // Current DataStax Enterprise - 'ALL'], - description: '''Apache Cassandra® or DataStax Enterprise server version to use for adhoc BUILD-AND-EXECUTE-TESTS builds - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ChoiceDescription
2.1Apache Cassandra® v2.1.x
2.2Apache Cassandra® v2.2.x
3.0Apache Cassandra® v3.0.x
3.11Apache Cassandra® v3.11.x
4.0Apache Cassandra® v4.x (CURRENTLY UNDER DEVELOPMENT)
dse-5.1DataStax Enterprise v5.1.x
dse-6.0DataStax Enterprise v6.0.x
dse-6.7DataStax Enterprise v6.7.x
dse-6.8.0DataStax Enterprise v6.8.0
''') - choice( - name: 'ADHOC_BUILD_AND_EXECUTE_TESTS_JABBA_VERSION', - choices: ['zulu@1.6', // Zulu JDK 1.6 - 'zulu@1.7', // Zulu JDK 1.7 - '1.8', // Oracle JDK version 1.8 (current default) - 'openjdk@1.11'], // OpenJDK version 11 - description: '''JDK version to use for TESTING when running adhoc BUILD-AND-EXECUTE-TESTS builds. All builds will use JDK8 for building the driver - - - - - - - - - - - - - - - - - - - - - - - -
ChoiceDescription
zulu@1.6Zulu JDK version 1.6
zulu@1.7Zulu JDK version 1.7
1.8Oracle JDK version 1.8 (Used for compiling regardless of choice)
openjdk@1.11OpenJDK version 11
''') - choice( - name: 'ADHOC_BUILD_AND_EXECUTE_TESTS_TEST_PROFILE', - choices: ['short', 'long'], - description: 'Test profile to execute during test phase of the build') - choice( - name: 'CI_SCHEDULE', - choices: ['DO-NOT-CHANGE-THIS-SELECTION', 'WEEKNIGHTS', 'WEEKENDS', 'MONTHLY'], - description: 'CI testing schedule to execute periodically scheduled builds and tests of the driver (DO NOT CHANGE THIS SELECTION)') - string( - name: 'CI_SCHEDULE_SERVER_VERSIONS', - defaultValue: 'DO-NOT-CHANGE-THIS-SELECTION', - description: 'CI testing server version(s) to utilize for scheduled test runs of the driver (DO NOT CHANGE THIS SELECTION)') - string( - name: 'CI_SCHEDULE_JABBA_VERSION', - defaultValue: 'DO-NOT-CHANGE-THIS-SELECTION', - description: 'CI testing JDK version(s) to utilize for scheduled test runs of the driver (DO NOT CHANGE THIS SELECTION)') - string( - name: 'CI_SCHEDULE_TEST_PROFILE', - defaultValue: 'DO-NOT-CHANGE-THIS-SELECTION', - description: 'CI testing profile to execute (DO NOT CHANGE THIS SELECTION)') - } - - triggers { - // schedules only run against release branches (i.e. 3.x, 4.x, 4.5.x, etc.) - parameterizedCron(branchPatternCron().matcher(env.BRANCH_NAME).matches() ? """ - # Every weeknight (Monday - Friday) around 3:00 AM - ### JDK8 tests against 2.1, 3.0, 3.11 and 4.0 - H 3 * * 1-5 %CI_SCHEDULE=WEEKNIGHTS;CI_SCHEDULE_SERVER_VERSIONS=2.1 3.0 3.11 4.0;CI_SCHEDULE_JABBA_VERSION=1.8;CI_SCHEDULE_TEST_PROFILE=long - # Every weekend (Sunday) around 2:00 PM - ### JDK11 tests against 2.1, 3.0, 3.11 and 4.0 - H 14 * * 0 %CI_SCHEDULE=WEEKENDS;CI_SCHEDULE_SERVER_VERSIONS=2.1 3.0 3.11 4.0;CI_SCHEDULE_JABBA_VERSION=openjdk@1.11;CI_SCHEDULE_TEST_PROFILE=long - """ : "") - } - - environment { - OS_VERSION = 'ubuntu/bionic64/java-driver' - JABBA_SHELL = '/usr/lib/jabba/jabba.sh' - CCM_ENVIRONMENT_SHELL = '/usr/local/bin/ccm_environment.sh' - } - - stages { - stage ('Per-Commit') { - options { - timeout(time: 2, unit: 'HOURS') - } - when { - beforeAgent true - allOf { - expression { params.ADHOC_BUILD_TYPE == 'BUILD' } - expression { params.CI_SCHEDULE == 'DO-NOT-CHANGE-THIS-SELECTION' } - expression { params.CI_SCHEDULE_SERVER_VERSIONS == 'DO-NOT-CHANGE-THIS-SELECTION' } - expression { params.CI_SCHEDULE_JABBA_VERSION == 'DO-NOT-CHANGE-THIS-SELECTION' } - expression { params.CI_SCHEDULE_TEST_PROFILE == 'DO-NOT-CHANGE-THIS-SELECTION' } - not { buildingTag() } - } - } - - matrix { - axes { - axis { - name 'SERVER_VERSION' - values '3.11', // Current Apache CassandraⓇ - '4.0' // Development Apache CassandraⓇ - } - } - - agent { - label "${OS_VERSION}" - } - environment { - // Per-commit builds are only going to run against JDK8 - JABBA_VERSION = '1.8' - TEST_PROFILE = 'short' - } - - stages { - stage('Initialize-Environment') { - steps { - initializeEnvironment() - script { - if (env.BUILD_STATED_SLACK_NOTIFIED != 'true') { - notifySlack() - } - } - } - } - stage('Describe-Build') { - steps { - describePerCommitStage() - } - } - stage('Build-Driver') { - steps { - buildDriver(env.JABBA_VERSION) - } - } - stage('Execute-Tests') { - steps { - catchError { - // Use the matrix JDK for testing - executeTests() - } - } - post { - always { - /* - * Empty results are possible - * - * - Build failures during mvn verify may exist so report may not be available - */ - junit testResults: '**/target/surefire-reports/TEST-*.xml', allowEmptyResults: true - junit testResults: '**/target/failsafe-reports/TEST-*.xml', allowEmptyResults: true - } - } - } - stage('Execute-Code-Coverage') { - // Ensure the code coverage is run only once per-commit - when { environment name: 'SERVER_VERSION', value: '4.0' } - steps { - executeCodeCoverage() - } - } - } - } - post { - aborted { - notifySlack('aborted') - } - success { - notifySlack('completed') - } - unstable { - notifySlack('unstable') - } - failure { - notifySlack('FAILED') - } - } - } - - stage('Adhoc-And-Scheduled-Testing') { - when { - beforeAgent true - allOf { - expression { (params.ADHOC_BUILD_TYPE == 'BUILD' && params.CI_SCHEDULE != 'DO-NOT-CHANGE-THIS-SELECTION') || - params.ADHOC_BUILD_TYPE == 'BUILD-AND-EXECUTE-TESTS' } - not { buildingTag() } - anyOf { - expression { params.ADHOC_BUILD_TYPE == 'BUILD-AND-EXECUTE-TESTS' } - allOf { - expression { params.ADHOC_BUILD_TYPE == 'BUILD' } - expression { params.CI_SCHEDULE != 'DO-NOT-CHANGE-THIS-SELECTION' } - expression { params.CI_SCHEDULE_SERVER_VERSIONS != 'DO-NOT-CHANGE-THIS-SELECTION' } - } - } - } - } - - environment { - SERVER_VERSIONS = "${params.CI_SCHEDULE_SERVER_VERSIONS == 'DO-NOT-CHANGE-THIS-SELECTION' ? params.ADHOC_BUILD_AND_EXECUTE_TESTS_SERVER_VERSION : params.CI_SCHEDULE_SERVER_VERSIONS}" - JABBA_VERSION = "${params.CI_SCHEDULE_JABBA_VERSION == 'DO-NOT-CHANGE-THIS-SELECTION' ? params.ADHOC_BUILD_AND_EXECUTE_TESTS_JABBA_VERSION : params.CI_SCHEDULE_JABBA_VERSION}" - TEST_PROFILE = "${params.CI_SCHEDULE_TEST_PROFILE == 'DO-NOT-CHANGE-THIS-SELECTION' ? params.ADHOC_BUILD_AND_EXECUTE_TESTS_TEST_PROFILE : params.CI_SCHEDULE_TEST_PROFILE}" - } - - matrix { - axes { - axis { - name 'SERVER_VERSION' - values '2.1', // Legacy Apache CassandraⓇ - '3.0', // Previous Apache CassandraⓇ - '3.11', // Current Apache CassandraⓇ - '4.0', // Development Apache CassandraⓇ - 'dse-5.1', // Legacy DataStax Enterprise - 'dse-6.0', // Previous DataStax Enterprise - 'dse-6.7', // Previous DataStax Enterprise - 'dse-6.8.0' // Current DataStax Enterprise - } - } - when { - beforeAgent true - allOf { - expression { return env.SERVER_VERSIONS.split(' ').any { it =~ /(ALL|${env.SERVER_VERSION})/ } } - } - } - agent { - label "${env.OS_VERSION}" - } - - stages { - stage('Initialize-Environment') { - steps { - initializeEnvironment() - script { - if (env.BUILD_STATED_SLACK_NOTIFIED != 'true') { - notifySlack() - } - } - } - } - stage('Describe-Build') { - steps { - describeAdhocAndScheduledTestingStage() - } - } - stage('Build-Driver') { - steps { - // Jabba default should be a JDK8 for now - buildDriver('default') - } - } - stage('Execute-Tests') { - steps { - catchError { - // Use the matrix JDK for testing - executeTests() - } - } - post { - always { - /* - * Empty results are possible - * - * - Build failures during mvn verify may exist so report may not be available - * - With boolean parameters to skip tests a failsafe report may not be available - */ - junit testResults: '**/target/surefire-reports/TEST-*.xml', allowEmptyResults: true - junit testResults: '**/target/failsafe-reports/TEST-*.xml', allowEmptyResults: true - } - } - } - stage('Execute-Code-Coverage') { - // Ensure the code coverage is run only once per-commit - when { - allOf { - environment name: 'SERVER_VERSION', value: '4.0' - environment name: 'JABBA_VERSION', value: '1.8' - } - } - steps { - executeCodeCoverage() - } - } - } - } - post { - aborted { - notifySlack('aborted') - } - success { - notifySlack('completed') - } - unstable { - notifySlack('unstable') - } - failure { - notifySlack('FAILED') - } - } - } - } -} diff --git a/README.md b/README.md index eef132ac85f..06baa564e02 100644 --- a/README.md +++ b/README.md @@ -1,135 +1,71 @@ # Datastax Java Driver for Apache Cassandra® -*If you're reading this on github.com, please note that this is the readme -for the development version and that some features described here might -not yet have been released. You can find the documentation for the latest -version through the [Java driver -docs](http://docs.datastax.com/en/developer/java-driver/3.11/index.html) or via the release tags, -[e.g. 3.11.5](https://github.com/datastax/java-driver/tree/3.11.5).* - -A modern, [feature-rich](manual/) and highly tunable Java client -library for Apache Cassandra (2.1+) and using exclusively Cassandra's binary protocol -and Cassandra Query Language v3. _Use the [DataStax Enterprise Java driver][dse-driver] -for better compatibility and support for DataStax Enterprise._ - -**Features:** - -* [Sync](manual/) and [Async](manual/async/) API -* [Simple](manual/statements/simple/), [Prepared](manual/statements/prepared/), and [Batch](manual/statements/batch/) - statements -* Asynchronous IO, parallel execution, request pipelining -* [Connection pooling](manual/pooling/) -* Auto node discovery -* Automatic reconnection -* Configurable [load balancing](manual/load_balancing/) and [retry policies](manual/retries/) -* Works with any cluster size -* [Query builder](manual/statements/built/) -* [Object mapper](manual/object_mapper/) - -The driver architecture is based on layers. At the bottom lies the driver core. -This core handles everything related to the connections to a Cassandra -cluster (for example, connection pool, discovering new nodes, etc.) and exposes a simple, -relatively low-level API on top of which higher level layers can be built. - -The driver contains the following modules: - -- driver-core: the core layer. -- driver-mapping: the object mapper. -- driver-extras: optional features for the Java driver. -- driver-examples: example applications using the other modules which are - only meant for demonstration purposes. -- driver-tests: tests for the java-driver. - -**Useful links:** - -- JIRA (bug tracking): https://datastax-oss.atlassian.net/browse/JAVA -- MAILING LIST: https://groups.google.com/a/lists.datastax.com/forum/#!forum/java-driver-user -- DATASTAX ACADEMY SLACK: #datastax-drivers on https://academy.datastax.com/slack -- TWITTER: [@dsJavaDriver](https://twitter.com/dsJavaDriver) tweets Java - driver releases and important announcements (low frequency). - [@DataStaxEng](https://twitter.com/datastaxeng) has more news including - other drivers, Cassandra, and DSE. -- DOCS: the [manual](http://docs.datastax.com/en/developer/java-driver/3.11/manual/) has quick - start material and technical details about the driver and its features. -- API: https://docs.datastax.com/en/drivers/java/3.11 -- GITHUB REPOSITORY: https://github.com/datastax/java-driver -- [changelog](changelog/) +*If you're reading this on github.com, please note that this is the readme for the development +version and that some features described here might not yet have been released. You can find the +documentation for latest version through [DataStax Docs] or via the release tags, e.g. +[4.0.0](https://github.com/datastax/java-driver/tree/4.0.0).* -## Getting the driver +A modern, feature-rich and highly tunable Java client library for [Apache Cassandra®] \(2.1+) and +[DataStax Enterprise] \(4.7+), using exclusively Cassandra's binary protocol and Cassandra Query +Language v3. -The last release of the driver is available on Maven Central. You can install -it in your application using the following Maven dependency (_if -using DataStax Enterprise, install the [DataStax Enterprise Java driver][dse-driver] instead_): +[DataStax Docs]: http://docs.datastax.com/en/developer/java-driver/ +[Apache Cassandra®]: http://cassandra.apache.org/ +[DataStax Enterprise]: http://www.datastax.com/products/datastax-enterprise -```xml - - com.datastax.cassandra - cassandra-driver-core - 3.11.5 - -``` +## Getting the driver -Note that the object mapper is published as a separate artifact: +The driver artifacts are published in Maven central, under the group id [com.datastax.oss]; there +are multiple modules, all prefixed with `java-driver-`. ```xml - com.datastax.cassandra - cassandra-driver-mapping - 3.11.5 + com.datastax.oss + java-driver-core + 4.0.0 -``` -The 'extras' module is also published as a separate artifact: - -```xml - com.datastax.cassandra - cassandra-driver-extras - 3.11.5/version> + com.datastax.oss + java-driver-query-builder + 4.0.0 ``` +Refer to the [manual] for more details. -We also provide a [shaded JAR](manual/shaded_jar/) -to avoid the explicit dependency to Netty. +[com.datastax.oss]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.datastax.oss%22 +[manual]: manual/ -If you can't use a dependency management tool, a -[binary tarball](https://github.com/datastax/java-driver/releases/3.11.5) -is available for download. +## Migrating from previous versions -## Compatibility +Java driver 4 is **not binary compatible** with previous versions. However, most of the concepts +remain unchanged, and the new API will look very familiar to 2.x and 3.x users. -The Java client driver 3.11.5 ([branch 3.x](https://github.com/datastax/java-driver/tree/3.x)) is compatible with Apache -Cassandra 2.1, 2.2 and 3.0+ (see [this page](http://docs.datastax.com/en/developer/java-driver/3.11/manual/native_protocol/) for -the most up-to-date compatibility information). +See the [upgrade guide](upgrade_guide/) for details. -UDT and tuple support is available only when using Apache Cassandra 2.1 or higher (see [CQL improvements in Cassandra 2.1](http://www.datastax.com/dev/blog/cql-in-2-1)). +## Useful links -Other features are available only when using Apache Cassandra 2.0 or higher (e.g. result set paging, -[BatchStatement](https://github.com/datastax/java-driver/blob/3.x/driver-core/src/main/java/com/datastax/driver/core/BatchStatement.java), -[lightweight transactions](http://www.datastax.com/documentation/cql/3.1/cql/cql_using/use_ltweight_transaction_t.html) --- see [What's new in Cassandra 2.0](http://www.datastax.com/documentation/cassandra/2.0/cassandra/features/features_key_c.html)). -Trying to use these with a cluster running Cassandra 1.2 will result in -an [UnsupportedFeatureException](https://github.com/datastax/java-driver/blob/3.x/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnsupportedFeatureException.java) being thrown. +* [Manual][manual] +* [API docs] +* Bug tracking: [JIRA] +* [Mailing list] +* Twitter: [@dsJavaDriver] tweets Java driver releases and important announcements (low frequency). + [@DataStaxEng] has more news, including other drivers, Cassandra, and DSE. +* [Changelog] +* [FAQ] -The java driver supports Java JDK versions 6 and above. - -If using _DataStax Enterprise_, the [DataStax Enterprise Java driver][dse-driver] provides -more features and better compatibility. - -__Disclaimer__: Some _DataStax/DataStax Enterprise_ products might partially work on -big-endian systems, but _DataStax_ does not officially support these systems. - -## Upgrading from previous versions - -If you are upgrading from a previous version of the driver, be sure to have a look at -the [upgrade guide](/upgrade_guide/). - -If you are upgrading to _DataStax Enterprise_, use the [DataStax Enterprise Java driver][dse-driver] for more -features and better compatibility. +[API docs]: http://www.datastax.com/drivers/java/4.0 +[JIRA]: https://datastax-oss.atlassian.net/browse/JAVA +[Mailing list]: https://groups.google.com/a/lists.datastax.com/forum/#!forum/java-driver-user +[@dsJavaDriver]: https://twitter.com/dsJavaDriver +[@DataStaxEng]: https://twitter.com/datastaxeng +[Changelog]: changelog/ +[FAQ]: faq/ ## License -© DataStax, Inc. + +Copyright 2017, DataStax Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -143,4 +79,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -[dse-driver]: http://docs.datastax.com/en/developer/java-driver-dse/latest/ +---- + +DataStax is a registered trademark of DataStax, Inc. and its subsidiaries in the United States +and/or other countries. + +Apache Cassandra, Apache, Tomcat, Lucene, Solr, Hadoop, Spark, TinkerPop, and Cassandra are +trademarks of the [Apache Software Foundation](http://www.apache.org/) or its subsidiaries in +Canada, the United States and/or other countries. diff --git a/build.yaml b/build.yaml new file mode 100644 index 00000000000..79dd3b2c84e --- /dev/null +++ b/build.yaml @@ -0,0 +1,22 @@ +java: + - openjdk8 +os: + - ubuntu/bionic64/java-driver +cassandra: + - '2.1' + - '2.2' + - '3.0' + - '3.11' +build: + - type: maven + version: 3.2.5 + goals: verify --batch-mode + properties: | + ccm.version=$CCM_CASSANDRA_VERSION + - xunit: + - "**/target/surefire-reports/TEST-*.xml" + - "**/target/failsafe-reports/TEST-*.xml" + - jacoco: true +disable_commit_status: true +notify: + slack: java-driver-dev-bots diff --git a/changelog/README.md b/changelog/README.md index 822b9f3fef1..abf3e12fe7a 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -1,1706 +1,218 @@ ## Changelog - - -## 3.11.5 -- [improvement] JAVA-3114: Shade io.dropwizard.metrics:metrics-core in shaded driver -- [improvement] JAVA-3115: SchemaChangeListener#onKeyspaceChanged can fire when keyspace has not changed if using SimpleStrategy replication - -## 3.11.4 -- [improvement] JAVA-3079: Upgrade Netty to 4.1.94, 3.x edition -- [improvement] JAVA-3082: Fix maven build for Apple-silicon -- [improvement] PR 1671: Fix LatencyAwarePolicy scale docstring - -## 3.11.3 -- [improvement] JAVA-3023: Upgrade Netty to 4.1.77, 3.x edition - - -## 3.11.2 -- [improvement] JAVA-3008: Upgrade Netty to 4.1.75, 3.x edition -- [improvement] JAVA-2984: Upgrade Jackson to resolve high-priority CVEs - - -## 3.11.1 -- [bug] JAVA-2967: Support native transport peer information for DSE 6.8. -- [bug] JAVA-2976: Support missing protocol v5 error codes CAS_WRITE_UNKNOWN, CDC_WRITE_FAILURE. - - -## 3.11.0 - -- [improvement] JAVA-2705: Remove protocol v5 beta status, add v6-beta. -- [bug] JAVA-2923: Detect and use Guava's new HostAndPort.getHost method. -- [bug] JAVA-2922: Switch to modern framing format inside a channel handler. -- [bug] JAVA-2924: Consider protocol version unsupported when server requires USE_BETA flag for it. - - -## 3.10.2 - -- [bug] JAVA-2860: Avoid NPE if channel initialization crashes. - - -## 3.10.1 - -- [bug] JAVA-2857: Fix NPE when built statements without parameters are logged at TRACE level. -- [bug] JAVA-2843: Successfully parse DSE table schema in OSS driver. - - -## 3.10.0 - -- [improvement] JAVA-2676: Don't reschedule flusher after empty runs. -- [new feature] JAVA-2772: Support new protocol v5 message format. - - -## 3.9.0 - -- [bug] JAVA-2627: Avoid logging error message including stack trace in request handler. -- [new feature] JAVA-2706: Add now_in_seconds to protocol v5 query messages. -- [improvement] JAVA-2730: Add support for Cassandra® 4.0 table options -- [improvement] JAVA-2702: Transient Replication Support for Cassandra® 4.0 - - -## 3.8.0 - -- [new feature] JAVA-2356: Support for DataStax Cloud API. -- [improvement] JAVA-2483: Allow to provide secure bundle via URL. -- [improvement] JAVA-2499: Allow to read the secure bundle from an InputStream. -- [improvement] JAVA-2457: Detect CaaS and change default consistency. -- [improvement] JAVA-2485: Add errors for Cloud misconfiguration. -- [documentation] JAVA-2504: Migrate Cloud "getting started" page to driver manual. -- [improvement] JAVA-2516: Enable hostname validation with Cloud -- [bug] JAVA-2515: NEW_NODE and REMOVED_NODE events should trigger ADDED and REMOVED. - - -### 3.7.2 - -- [bug] JAVA-2249: Stop stripping trailing zeros in ByteOrderedTokens. -- [bug] JAVA-1492: Don't immediately reuse busy connections for another request. -- [bug] JAVA-2198: Handle UDTs with names that clash with collection types. -- [bug] JAVA-2204: Avoid memory leak when client holds onto a stale TableMetadata instance. - - -### 3.7.1 - -- [bug] JAVA-2174: Metadata.needsQuote should accept empty strings. -- [bug] JAVA-2193: Fix flaky tests in WarningsTest. - - -### 3.7.0 - -- [improvement] JAVA-2025: Include exception message in Abstract\*Codec.accepts(null). -- [improvement] JAVA-1980: Use covariant return types in RemoteEndpointAwareJdkSSLOptions.Builder methods. -- [documentation] JAVA-2062: Document frozen collection preference with Mapper. -- [bug] JAVA-2071: Fix NPE in ArrayBackedRow.toString(). -- [bug] JAVA-2070: Call onRemove instead of onDown when rack and/or DC information changes for a host. -- [improvement] JAVA-1256: Log parameters of BuiltStatement in QueryLogger. -- [documentation] JAVA-2074: Document preference for LZ4 over Snappy. -- [bug] JAVA-1612: Include netty-common jar in binary tarball. -- [improvement] JAVA-2003: Simplify CBUtil internal API to improve performance. -- [improvement] JAVA-2002: Reimplement TypeCodec.accepts to improve performance. -- [documentation] JAVA-2041: Deprecate cross-DC failover in DCAwareRoundRobinPolicy. -- [documentation] JAVA-1159: Document workaround for using tuple with udt field in Mapper. -- [documentation] JAVA-1964: Complete remaining "Coming Soon" sections in docs. -- [improvement] JAVA-1950: Log server side warnings returned from a query. -- [improvement] JAVA-2123: Allow to use QueryBuilder for building queries against Materialized Views. -- [bug] JAVA-2082: Avoid race condition during cluster close and schema refresh. - - -### 3.6.0 - -- [improvement] JAVA-1394: Add request-queue-depth metric. -- [improvement] JAVA-1857: Add Statement.setHost. -- [bug] JAVA-1920: Use nanosecond precision in LocalTimeCodec#format(). -- [bug] JAVA-1794: Driver tries to create a connection array of size -1. -- [new feature] JAVA-1899: Support virtual tables. -- [bug] JAVA-1908: TableMetadata.asCQLQuery does not add table option 'memtable_flush_period_in_ms' in the generated query. -- [bug] JAVA-1924: StatementWrapper setters should return the wrapping statement. -- [new feature] JAVA-1532: Add Codec support for Java 8's LocalDateTime and ZoneId. -- [improvement] JAVA-1786: Use Google code formatter. -- [bug] JAVA-1871: Change LOCAL\_SERIAL.isDCLocal() to return true. -- [documentation] JAVA-1902: Clarify unavailable & request error in DefaultRetryPolicy javadoc. -- [new feature] JAVA-1903: Add WhiteListPolicy.ofHosts. -- [bug] JAVA-1928: Fix GuavaCompatibility for Guava 26. -- [bug] JAVA-1935: Add null check in QueryConsistencyException.getHost. -- [improvement] JAVA-1771: Send driver name and version in STARTUP message. -- [improvement] JAVA-1388: Add dynamic port discovery for system.peers\_v2. -- [documentation] JAVA-1810: Note which setters are not propagated to PreparedStatement. -- [bug] JAVA-1944: Surface Read and WriteFailureException to RetryPolicy. -- [bug] JAVA-1211: Fix NPE in cluster close when cluster init fails. -- [bug] JAVA-1220: Fail fast on cluster init if previous init failed. -- [bug] JAVA-1929: Preempt session execute queries if session was closed. - -Merged from 3.5.x: - -- [bug] JAVA-1872: Retain table's views when processing table update. - - -### 3.5.0 - -- [improvement] JAVA-1448: TokenAwarePolicy should respect child policy ordering. -- [bug] JAVA-1751: Include defaultTimestamp length in encodedSize for protocol version >= 3. -- [bug] JAVA-1770: Fix message size when using Custom Payload. -- [documentation] JAVA-1760: Add metrics documentation. -- [improvement] JAVA-1765: Update dependencies to latest patch versions. -- [improvement] JAVA-1752: Deprecate DowngradingConsistencyRetryPolicy. -- [improvement] JAVA-1735: Log driver version on first use. -- [documentation] JAVA-1380: Add FAQ entry for errors arising from incompatibilities. -- [improvement] JAVA-1748: Support IS NOT NULL and != in query builder. -- [documentation] JAVA-1740: Mention C*2.2/3.0 incompatibilities in paging state manual. -- [improvement] JAVA-1725: Add a getNodeCount method to CCMAccess for easier automation. -- [new feature] JAVA-708: Add means to measure request sizes. -- [documentation] JAVA-1788: Add example for enabling host name verification to SSL docs. -- [improvement] JAVA-1791: Revert "JAVA-1677: Warn if auth is configured on the client but not the server." -- [bug] JAVA-1789: Account for flags in Prepare encodedSize. -- [bug] JAVA-1797: Use jnr-ffi version required by jnr-posix. - - -### 3.4.0 - -- [improvement] JAVA-1671: Remove unnecessary test on prepared statement metadata. -- [bug] JAVA-1694: Upgrade to jackson-databind 2.7.9.2 to address CVE-2015-15095. -- [documentation] JAVA-1685: Clarify recommendation on preparing SELECT *. -- [improvement] JAVA-1679: Improve error message on batch log write timeout. -- [improvement] JAVA-1672: Remove schema agreement check when repreparing on up. -- [improvement] JAVA-1677: Warn if auth is configured on the client but not the server. -- [new feature] JAVA-1651: Add NO_COMPACT startup option. -- [improvement] JAVA-1683: Add metrics to track writes to nodes. -- [new feature] JAVA-1229: Allow specifying the keyspace for individual queries. -- [improvement] JAVA-1682: Provide a way to record latencies for cancelled speculative executions. -- [improvement] JAVA-1717: Add metrics to latency-aware policy. -- [improvement] JAVA-1675: Remove dates from copyright headers. - -Merged from 3.3.x: - -- [bug] JAVA-1555: Include VIEW and CDC in WriteType. -- [bug] JAVA-1599: exportAsString improvements (sort, format, clustering order) -- [improvement] JAVA-1587: Deterministic ordering of columns used in Mapper#saveQuery -- [improvement] JAVA-1500: Add a metric to report number of in-flight requests. -- [bug] JAVA-1438: QueryBuilder check for empty orderings. -- [improvement] JAVA-1490: Allow zero delay for speculative executions. -- [documentation] JAVA-1607: Add FAQ entry for netty-transport-native-epoll. -- [bug] JAVA-1630: Fix Metadata.addIfAbsent. -- [improvement] JAVA-1619: Update QueryBuilder methods to support Iterable input. -- [improvement] JAVA-1527: Expose host_id and schema_version on Host metadata. -- [new feature] JAVA-1377: Add support for TWCS in SchemaBuilder. -- [improvement] JAVA-1631: Publish a sources jar for driver-core-tests. -- [improvement] JAVA-1632: Add a withIpPrefix(String) method to CCMBridge.Builder. -- [bug] JAVA-1639: VersionNumber does not fullfill equals/hashcode contract. -- [bug] JAVA-1613: Fix broken shaded Netty detection in NettyUtil. -- [bug] JAVA-1666: Fix keyspace export when a UDT has case-sensitive field names. -- [improvement] JAVA-1196: Include hash of result set metadata in prepared statement id. -- [improvement] JAVA-1670: Support user-provided JMX ports for CCMBridge. -- [improvement] JAVA-1661: Avoid String.toLowerCase if possible in Metadata. -- [improvement] JAVA-1659: Expose low-level flusher tuning options. -- [improvement] JAVA-1660: Support netty-transport-native-epoll in OSGi container. - - -### 3.3.2 - -- [bug] JAVA-1666: Fix keyspace export when a UDT has case-sensitive field names. -- [improvement] JAVA-1196: Include hash of result set metadata in prepared statement id. -- [improvement] JAVA-1670: Support user-provided JMX ports for CCMBridge. -- [improvement] JAVA-1661: Avoid String.toLowerCase if possible in Metadata. -- [improvement] JAVA-1659: Expose low-level flusher tuning options. -- [improvement] JAVA-1660: Support netty-transport-native-epoll in OSGi container. - - -### 3.3.1 - -- [bug] JAVA-1555: Include VIEW and CDC in WriteType. -- [bug] JAVA-1599: exportAsString improvements (sort, format, clustering order) -- [improvement] JAVA-1587: Deterministic ordering of columns used in Mapper#saveQuery -- [improvement] JAVA-1500: Add a metric to report number of in-flight requests. -- [bug] JAVA-1438: QueryBuilder check for empty orderings. -- [improvement] JAVA-1490: Allow zero delay for speculative executions. -- [documentation] JAVA-1607: Add FAQ entry for netty-transport-native-epoll. -- [bug] JAVA-1630: Fix Metadata.addIfAbsent. -- [improvement] JAVA-1619: Update QueryBuilder methods to support Iterable input. -- [improvement] JAVA-1527: Expose host_id and schema_version on Host metadata. -- [new feature] JAVA-1377: Add support for TWCS in SchemaBuilder. -- [improvement] JAVA-1631: Publish a sources jar for driver-core-tests. -- [improvement] JAVA-1632: Add a withIpPrefix(String) method to CCMBridge.Builder. -- [bug] JAVA-1639: VersionNumber does not fullfill equals/hashcode contract. -- [bug] JAVA-1613: Fix broken shaded Netty detection in NettyUtil. - - -### 3.3.0 - -- [bug] JAVA-1469: Update LoggingRetryPolicy to deal with SLF4J-353. -- [improvement] JAVA-1203: Upgrade Metrics to allow usage in OSGi. -- [bug] JAVA-1407: KeyspaceMetadata exportAsString should export user types in topological sort order. -- [bug] JAVA-1455: Mapper support using unset for null values. -- [bug] JAVA-1464: Allow custom codecs with non public constructors in @Param. -- [bug] JAVA-1470: Querying multiple pages overrides WrappedStatement. -- [improvement] JAVA-1428: Upgrade logback and jackson dependencies. -- [documentation] JAVA-1463: Revisit speculative execution docs. -- [documentation] JAVA-1466: Revisit timestamp docs. -- [documentation] JAVA-1445: Clarify how nodes are penalized in LatencyAwarePolicy docs. -- [improvement] JAVA-1446: Support 'DEFAULT UNSET' in Query Builder JSON Insert. -- [improvement] JAVA-1443: Add groupBy method to Select statement. -- [improvement] JAVA-1458: Check thread in mapper sync methods. -- [improvement] JAVA-1488: Upgrade Netty to 4.0.47.Final. -- [improvement] JAVA-1460: Add speculative execution number to ExecutionInfo -- [improvement] JAVA-1431: Improve error handling during pool initialization. - - -### 3.2.0 - -- [new feature] JAVA-1347: Add support for duration type. -- [new feature] JAVA-1248: Implement "beta" flag for native protocol v5. -- [new feature] JAVA-1362: Send query options flags as [int] for Protocol V5+. -- [new feature] JAVA-1364: Enable creation of SSLHandler with remote address information. -- [improvement] JAVA-1367: Make protocol negotiation more resilient. -- [bug] JAVA-1397: Handle duration as native datatype in protocol v5+. -- [improvement] JAVA-1308: CodecRegistry performance improvements. -- [improvement] JAVA-1287: Add CDC to TableOptionsMetadata and Schema Builder. -- [improvement] JAVA-1392: Reduce lock contention in RPTokenFactory. -- [improvement] JAVA-1328: Provide compatibility with Guava 20. -- [improvement] JAVA-1247: Disable idempotence warnings. -- [improvement] JAVA-1286: Support setting and retrieving udt fields in QueryBuilder. -- [bug] JAVA-1415: Correctly report if a UDT column is frozen. -- [bug] JAVA-1418: Make Guava version detection more reliable. -- [new feature] JAVA-1174: Add ifNotExists option to mapper. -- [improvement] JAVA-1414: Optimize Metadata.escapeId and Metadata.handleId. -- [improvement] JAVA-1310: Make mapper's ignored properties configurable. -- [improvement] JAVA-1316: Add strategy for resolving properties into CQL names. -- [bug] JAVA-1424: Handle new WRITE_FAILURE and READ_FAILURE format in v5 protocol. - -Merged from 3.1.x branch: - -- [bug] JAVA-1371: Reintroduce connection pool timeout. -- [bug] JAVA-1313: Copy SerialConsistencyLevel to PreparedStatement. -- [documentation] JAVA-1334: Clarify documentation of method `addContactPoints`. -- [improvement] JAVA-1357: Document that getReplicas only returns replicas of the last token in range. -- [bug] JAVA-1404: Fix min token handling in TokenRange.contains. -- [bug] JAVA-1429: Prevent heartbeats until connection is fully initialized. - - -### 3.1.4 - -Merged from 3.0.x branch: - -- [bug] JAVA-1371: Reintroduce connection pool timeout. -- [bug] JAVA-1313: Copy SerialConsistencyLevel to PreparedStatement. -- [documentation] JAVA-1334: Clarify documentation of method `addContactPoints`. -- [improvement] JAVA-1357: Document that getReplicas only returns replicas of the last token in range. - - -### 3.1.3 - -Merged from 3.0.x branch: - -- [bug] JAVA-1330: Add un/register for SchemaChangeListener in DelegatingCluster -- [bug] JAVA-1351: Include Custom Payload in Request.copy. -- [bug] JAVA-1346: Reset heartbeat only on client reads (not writes). -- [improvement] JAVA-866: Support tuple notation in QueryBuilder.eq/in. - - -### 3.1.2 - -- [bug] JAVA-1321: Wrong OSGi dependency version for Guava. - -Merged from 3.0.x branch: - -- [bug] JAVA-1312: QueryBuilder modifies selected columns when manually selected. -- [improvement] JAVA-1303: Add missing BoundStatement.setRoutingKey(ByteBuffer...) -- [improvement] JAVA-262: Make internal executors customizable - - -### 3.1.1 - -- [bug] JAVA-1284: ClockFactory should check system property before attempting to load Native class. -- [bug] JAVA-1255: Allow nested UDTs to be used in Mapper. -- [bug] JAVA-1279: Mapper should exclude Groovy's "metaClass" property when looking for mapped properties - -Merged from 3.0.x branch: - -- [improvement] JAVA-1246: Driver swallows the real exception in a few cases -- [improvement] JAVA-1261: Throw error when attempting to page in I/O thread. -- [bug] JAVA-1258: Regression: Mapper cannot map a materialized view after JAVA-1126. -- [bug] JAVA-1101: Batch and BatchStatement should consider inner statements to determine query idempotence -- [improvement] JAVA-1262: Use ParseUtils for quoting & unquoting. -- [improvement] JAVA-1275: Use Netty's default thread factory -- [bug] JAVA-1285: QueryBuilder routing key auto-discovery should handle case-sensitive column names. -- [bug] JAVA-1283: Don't cache failed query preparations in the mapper. -- [improvement] JAVA-1277: Expose AbstractSession.checkNotInEventLoop. -- [bug] JAVA-1272: BuiltStatement not able to print its query string if it contains mapped UDTs. -- [bug] JAVA-1292: 'Adjusted frame length' error breaks driver's ability to read data. -- [improvement] JAVA-1293: Make DecoderForStreamIdSize.MAX_FRAME_LENGTH configurable. -- [improvement] JAVA-1053: Add a metric for authentication errors -- [improvement] JAVA-1263: Eliminate unnecessary memory copies in FrameCompressor implementations. -- [improvement] JAVA-893: Make connection pool non-blocking - - -### 3.1.0 - -- [new feature] JAVA-1153: Add PER PARTITION LIMIT to Select QueryBuilder. -- [improvement] JAVA-743: Add JSON support to QueryBuilder. -- [improvement] JAVA-1233: Update HdrHistogram to 2.1.9. -- [improvement] JAVA-1233: Update Snappy to 1.1.2.6. -- [bug] JAVA-1161: Preserve full time zone info in ZonedDateTimeCodec and DateTimeCodec. -- [new feature] JAVA-1157: Allow asynchronous paging of Mapper Result. -- [improvement] JAVA-1212: Don't retry non-idempotent statements by default. -- [improvement] JAVA-1192: Make EventDebouncer settings updatable at runtime. -- [new feature] JAVA-541: Add polymorphism support to object mapper. -- [new feature] JAVA-636: Allow @Column annotations on getters/setters as well as fields. -- [new feature] JAVA-984: Allow non-void setters in object mapping. -- [new feature] JAVA-1055: Add ErrorAware load balancing policy. - -Merged from 3.0.x branch: - -- [bug] JAVA-1179: Request objects should be copied when executed. -- [improvement] JAVA-1182: Throw error when synchronous call made on I/O thread. -- [bug] JAVA-1184: Unwrap StatementWrappers when extracting column definitions. -- [bug] JAVA-1132: Executing bound statement with no variables results in exception with protocol v1. -- [improvement] JAVA-1040: SimpleStatement parameters support in QueryLogger. -- [improvement] JAVA-1151: Fail fast if HdrHistogram is not in the classpath. -- [improvement] JAVA-1154: Allow individual Statement to cancel the read timeout. -- [bug] JAVA-1074: Fix documentation around default timestamp generator. -- [improvement] JAVA-1109: Document SSLOptions changes in upgrade guide. -- [improvement] JAVA-1065: Add method to create token from partition key values. -- [improvement] JAVA-1136: Enable JDK signature check in module driver-extras. -- [improvement] JAVA-866: Support tuple notation in QueryBuilder.eq/in. -- [bug] JAVA-1140: Use same connection to check for schema agreement after a DDL query. -- [improvement] JAVA-1113: Support Cassandra 3.4 LIKE operator in QueryBuilder. -- [improvement] JAVA-1086: Support Cassandra 3.2 CAST function in QueryBuilder. -- [bug] JAVA-1095: Check protocol version for custom payload before sending the query. -- [improvement] JAVA-1133: Add OSGi headers to cassandra-driver-extras. -- [bug] JAVA-1137: Incorrect string returned by DataType.asFunctionParameterString() for collections and tuples. -- [bug] JAVA-1046: (Dynamic)CompositeTypes need to be parsed as string literal, not blob. -- [improvement] JAVA-1164: Clarify documentation on Host.listenAddress and broadcastAddress. -- [improvement] JAVA-1171: Add Host method to determine if DSE Graph is enabled. -- [improvement] JAVA-1069: Bootstrap driver-examples module. -- [documentation] JAVA-1150: Add example and FAQ entry about ByteBuffer/BLOB. -- [improvement] JAVA-1011: Expose PoolingOptions default values. -- [improvement] JAVA-630: Don't process DOWN events for nodes that have active connections. -- [improvement] JAVA-851: Improve UUIDs javadoc with regard to user-provided timestamps. -- [improvement] JAVA-979: Update javadoc for RegularStatement toString() and getQueryString() to indicate that consistency level and other parameters are not maintained in the query string. -- [bug] JAVA-1068: Unwrap StatementWrappers when hashing the paging state. -- [improvement] JAVA-1021: Improve error message when connect() is called with an invalid keyspace name. -- [improvement] JAVA-879: Mapper.map() accepts mapper-generated and user queries. -- [bug] JAVA-1100: Exception when connecting with shaded java driver in OSGI -- [bug] JAVA-1064: getTable create statement doesn't properly handle quotes in primary key. -- [bug] JAVA-1089: Set LWT made from BuiltStatements to non-idempotent. -- [improvement] JAVA-923: Position idempotent flag on object mapper queries. -- [bug] JAVA-1070: The Mapper should not prepare queries synchronously. -- [new feature] JAVA-982: Introduce new method ConsistencyLevel.isSerial(). -- [bug] JAVA-764: Retry with the normal consistency level (not the serial one) when a write times out on the Paxos phase. -- [improvement] JAVA-852: Ignore peers with null entries during discovery. -- [bug] JAVA-1005: DowngradingConsistencyRetryPolicy does not work with EACH_QUORUM when 1 DC is down. -- [bug] JAVA-1002: Avoid deadlock when re-preparing a statement on other hosts. -- [bug] JAVA-1072: Ensure defunct connections are properly evicted from the pool. -- [bug] JAVA-1152: Fix NPE at ControlConnection.refreshNodeListAndTokenMap(). - -Merged from 2.1 branch: - -- [improvement] JAVA-1038: Fetch node info by rpc_address if its broadcast_address is not in system.peers. -- [improvement] JAVA-888: Add cluster-wide percentile tracker. -- [improvement] JAVA-963: Automatically register PercentileTracker from components that use it. -- [new feature] JAVA-1019: SchemaBuilder support for CREATE/ALTER/DROP KEYSPACE. -- [bug] JAVA-727: Allow monotonic timestamp generators to drift in the future + use microsecond precision when possible. -- [improvement] JAVA-444: Add Java process information to UUIDs.makeNode() hash. - - -### 3.0.7 - -- [bug] JAVA-1371: Reintroduce connection pool timeout. -- [bug] JAVA-1313: Copy SerialConsistencyLevel to PreparedStatement. -- [documentation] JAVA-1334: Clarify documentation of method `addContactPoints`. -- [improvement] JAVA-1357: Document that getReplicas only returns replicas of the last token in range. - - -### 3.0.6 - -- [bug] JAVA-1330: Add un/register for SchemaChangeListener in DelegatingCluster -- [bug] JAVA-1351: Include Custom Payload in Request.copy. -- [bug] JAVA-1346: Reset heartbeat only on client reads (not writes). -- [improvement] JAVA-866: Support tuple notation in QueryBuilder.eq/in. - - -### 3.0.5 - -- [bug] JAVA-1312: QueryBuilder modifies selected columns when manually selected. -- [improvement] JAVA-1303: Add missing BoundStatement.setRoutingKey(ByteBuffer...) -- [improvement] JAVA-262: Make internal executors customizable -- [bug] JAVA-1320: prevent unnecessary task creation on empty pool - - -### 3.0.4 - -- [improvement] JAVA-1246: Driver swallows the real exception in a few cases -- [improvement] JAVA-1261: Throw error when attempting to page in I/O thread. -- [bug] JAVA-1258: Regression: Mapper cannot map a materialized view after JAVA-1126. -- [bug] JAVA-1101: Batch and BatchStatement should consider inner statements to determine query idempotence -- [improvement] JAVA-1262: Use ParseUtils for quoting & unquoting. -- [improvement] JAVA-1275: Use Netty's default thread factory -- [bug] JAVA-1285: QueryBuilder routing key auto-discovery should handle case-sensitive column names. -- [bug] JAVA-1283: Don't cache failed query preparations in the mapper. -- [improvement] JAVA-1277: Expose AbstractSession.checkNotInEventLoop. -- [bug] JAVA-1272: BuiltStatement not able to print its query string if it contains mapped UDTs. -- [bug] JAVA-1292: 'Adjusted frame length' error breaks driver's ability to read data. -- [improvement] JAVA-1293: Make DecoderForStreamIdSize.MAX_FRAME_LENGTH configurable. -- [improvement] JAVA-1053: Add a metric for authentication errors -- [improvement] JAVA-1263: Eliminate unnecessary memory copies in FrameCompressor implementations. -- [improvement] JAVA-893: Make connection pool non-blocking - - -### 3.0.3 - -- [improvement] JAVA-1147: Upgrade Netty to 4.0.37. -- [bug] JAVA-1213: Allow updates and inserts to BLOB column using read-only ByteBuffer. -- [bug] JAVA-1209: ProtocolOptions.getProtocolVersion() should return null instead of throwing NPE if Cluster has not - been init'd. -- [improvement] JAVA-1204: Update documentation to indicate tcnative version requirement. -- [bug] JAVA-1186: Fix duplicated hosts in DCAwarePolicy warn message. -- [bug] JAVA-1187: Fix warning message when local CL used with RoundRobinPolicy. -- [improvement] JAVA-1175: Warn if DCAwarePolicy configuration is inconsistent. -- [bug] JAVA-1139: ConnectionException.getMessage() throws NPE if address is null. -- [bug] JAVA-1202: Handle null rpc_address when checking schema agreement. -- [improvement] JAVA-1198: Document that BoundStatement is not thread-safe. -- [improvement] JAVA-1200: Upgrade LZ4 to 1.3.0. -- [bug] JAVA-1232: Fix NPE in IdempotenceAwareRetryPolicy.isIdempotent. -- [improvement] JAVA-1227: Document "SELECT *" issue with prepared statement. -- [bug] JAVA-1160: Fix NPE in VersionNumber.getPreReleaseLabels(). -- [improvement] JAVA-1126: Handle schema changes in Mapper. -- [bug] JAVA-1193: Refresh token and replica metadata synchronously when schema is altered. -- [bug] JAVA-1120: Skip schema refresh debouncer when checking for agreement as a result of schema change made by client. -- [improvement] JAVA-1242: Fix driver-core dependency in driver-stress -- [improvement] JAVA-1235: Move the query to the end of "re-preparing .." log message as a key value. - - -### 3.0.2 - -Merged from 2.1 branch: - -- [bug] JAVA-1179: Request objects should be copied when executed. -- [improvement] JAVA-1182: Throw error when synchronous call made on I/O thread. -- [bug] JAVA-1184: Unwrap StatementWrappers when extracting column definitions. - - -### 3.0.1 - -- [bug] JAVA-1132: Executing bound statement with no variables results in exception with protocol v1. -- [improvement] JAVA-1040: SimpleStatement parameters support in QueryLogger. -- [improvement] JAVA-1151: Fail fast if HdrHistogram is not in the classpath. -- [improvement] JAVA-1154: Allow individual Statement to cancel the read timeout. -- [bug] JAVA-1074: Fix documentation around default timestamp generator. -- [improvement] JAVA-1109: Document SSLOptions changes in upgrade guide. -- [improvement] JAVA-1065: Add method to create token from partition key values. -- [improvement] JAVA-1136: Enable JDK signature check in module driver-extras. -- [improvement] JAVA-866: Support tuple notation in QueryBuilder.eq/in. -- [bug] JAVA-1140: Use same connection to check for schema agreement after a DDL query. -- [improvement] JAVA-1113: Support Cassandra 3.4 LIKE operator in QueryBuilder. -- [improvement] JAVA-1086: Support Cassandra 3.2 CAST function in QueryBuilder. -- [bug] JAVA-1095: Check protocol version for custom payload before sending the query. -- [improvement] JAVA-1133: Add OSGi headers to cassandra-driver-extras. -- [bug] JAVA-1137: Incorrect string returned by DataType.asFunctionParameterString() for collections and tuples. -- [bug] JAVA-1046: (Dynamic)CompositeTypes need to be parsed as string literal, not blob. -- [improvement] JAVA-1164: Clarify documentation on Host.listenAddress and broadcastAddress. -- [improvement] JAVA-1171: Add Host method to determine if DSE Graph is enabled. -- [improvement] JAVA-1069: Bootstrap driver-examples module. -- [documentation] JAVA-1150: Add example and FAQ entry about ByteBuffer/BLOB. - -Merged from 2.1 branch: - -- [improvement] JAVA-1011: Expose PoolingOptions default values. -- [improvement] JAVA-630: Don't process DOWN events for nodes that have active connections. -- [improvement] JAVA-851: Improve UUIDs javadoc with regard to user-provided timestamps. -- [improvement] JAVA-979: Update javadoc for RegularStatement toString() and getQueryString() to indicate that consistency level and other parameters are not maintained in the query string. -- [bug] JAVA-1068: Unwrap StatementWrappers when hashing the paging state. -- [improvement] JAVA-1021: Improve error message when connect() is called with an invalid keyspace name. -- [improvement] JAVA-879: Mapper.map() accepts mapper-generated and user queries. -- [bug] JAVA-1100: Exception when connecting with shaded java driver in OSGI -- [bug] JAVA-1064: getTable create statement doesn't properly handle quotes in primary key. -- [bug] JAVA-1089: Set LWT made from BuiltStatements to non-idempotent. -- [improvement] JAVA-923: Position idempotent flag on object mapper queries. -- [bug] JAVA-1070: The Mapper should not prepare queries synchronously. -- [new feature] JAVA-982: Introduce new method ConsistencyLevel.isSerial(). -- [bug] JAVA-764: Retry with the normal consistency level (not the serial one) when a write times out on the Paxos phase. -- [improvement] JAVA-852: Ignore peers with null entries during discovery. -- [bug] JAVA-1005: DowngradingConsistencyRetryPolicy does not work with EACH_QUORUM when 1 DC is down. -- [bug] JAVA-1002: Avoid deadlock when re-preparing a statement on other hosts. -- [bug] JAVA-1072: Ensure defunct connections are properly evicted from the pool. -- [bug] JAVA-1152: Fix NPE at ControlConnection.refreshNodeListAndTokenMap(). - - -### 3.0.0 - -- [bug] JAVA-1034: fix metadata parser for collections of custom types. -- [improvement] JAVA-1035: Expose host broadcast_address and listen_address if available. -- [new feature] JAVA-1037: Allow named parameters in simple statements. -- [improvement] JAVA-1033: Allow per-statement read timeout. -- [improvement] JAVA-1042: Include DSE version and workload in Host data. - -Merged from 2.1 branch: - -- [improvement] JAVA-1030: Log token to replica map computation times. -- [bug] JAVA-1039: Minor bugs in Event Debouncer. - - -### 3.0.0-rc1 - -- [bug] JAVA-890: fix mapper for case-sensitive UDT. - - -### 3.0.0-beta1 - -- [bug] JAVA-993: Support for "custom" types after CASSANDRA-10365. -- [bug] JAVA-999: Handle unset parameters in QueryLogger. -- [bug] JAVA-998: SchemaChangeListener not invoked for Functions or Aggregates having UDT arguments. -- [bug] JAVA-1009: use CL ONE to compute query plan when reconnecting - control connection. -- [improvement] JAVA-1003: Change default consistency level to LOCAL_ONE (amends JAVA-926). -- [improvement] JAVA-863: Idempotence propagation in prepared statements. -- [improvement] JAVA-996: Make CodecRegistry available to ProtocolDecoder. -- [bug] JAVA-819: Driver shouldn't retry on client timeout if statement is not idempotent. -- [improvement] JAVA-1007: Make SimpleStatement and QueryBuilder "detached" again. - -Merged from 2.1 branch: - -- [improvement] JAVA-989: Include keyspace name when invalid replication found when generating token map. -- [improvement] JAVA-664: Reduce heap consumption for TokenMap. -- [bug] JAVA-994: Don't call on(Up|Down|Add|Remove) methods if Cluster is closed/closing. - - -### 3.0.0-alpha5 - -- [improvement] JAVA-958: Make TableOrView.Order visible. -- [improvement] JAVA-968: Update metrics to the latest version. -- [improvement] JAVA-965: Improve error handling for when a non-type 1 UUID is given to bind() on a timeuuid column. -- [improvement] JAVA-885: Pass the authenticator name from the server to the auth provider. -- [improvement] JAVA-961: Raise an exception when an older version of guava (<16.01) is found. -- [bug] JAVA-972: TypeCodec.parse() implementations should be case insensitive when checking for keyword NULL. -- [bug] JAVA-971: Make type codecs invariant. -- [bug] JAVA-986: Update documentation links to reference 3.0. -- [improvement] JAVA-841: Refactor SSLOptions API. -- [improvement] JAVA-948: Don't limit cipher suites by default. -- [improvement] JAVA-917: Document SSL configuration. -- [improvement] JAVA-936: Adapt schema metadata parsing logic to new storage format of CQL types in C* 3.0. -- [new feature] JAVA-846: Provide custom codecs library as an extra module. -- [new feature] JAVA-742: Codec Support for JSON. -- [new feature] JAVA-606: Codec support for Java 8. -- [new feature] JAVA-565: Codec support for Java arrays. -- [new feature] JAVA-605: Codec support for Java enums. -- [bug] JAVA-884: Fix UDT mapper to process fields in the correct order. - -Merged from 2.1 branch: - -- [bug] JAVA-854: avoid early return in Cluster.init when a node doesn't support the protocol version. -- [bug] JAVA-978: Fix quoting issue that caused Mapper.getTableMetadata() to return null. -- [improvement] JAVA-920: Downgrade "error creating pool" message to WARN. -- [bug] JAVA-954: Don't trigger reconnection before initialization complete. -- [improvement] JAVA-914: Avoid rejected tasks at shutdown. -- [improvement] JAVA-921: Add SimpleStatement.getValuesCount(). -- [bug] JAVA-901: Move call to connection.release() out of cancelHandler. -- [bug] JAVA-960: Avoid race in control connection shutdown. -- [bug] JAVA-656: Fix NPE in ControlConnection.updateLocationInfo. -- [bug] JAVA-966: Count uninitialized connections in conviction policy. -- [improvement] JAVA-917: Document SSL configuration. -- [improvement] JAVA-652: Add DCAwareRoundRobinPolicy builder. -- [improvement] JAVA-808: Add generic filtering policy that can be used to exclude specific DCs. -- [bug] JAVA-988: Metadata.handleId should handle escaped double quotes. -- [bug] JAVA-983: QueryBuilder cannot handle collections containing function calls. - - -### 3.0.0-alpha4 - -- [improvement] JAVA-926: Change default consistency level to LOCAL_QUORUM. -- [bug] JAVA-942: Fix implementation of UserType.hashCode(). -- [improvement] JAVA-877: Don't delay UP/ADDED notifications if protocol version = V4. -- [improvement] JAVA-938: Parse 'extensions' column in table metadata. -- [bug] JAVA-900: Fix Configuration builder to allow disabled metrics. -- [new feature] JAVA-902: Prepare API for async query trace. -- [new feature] JAVA-930: Add BoundStatement#unset. -- [bug] JAVA-946: Make table metadata options class visible. -- [bug] JAVA-939: Add crcCheckChance to TableOptionsMetadata#equals/hashCode. -- [bug] JAVA-922: Make TypeCodec return mutable collections. -- [improvement] JAVA-932: Limit visibility of codec internals. -- [improvement] JAVA-934: Warn if a custom codec collides with an existing one. -- [improvement] JAVA-940: Allow typed getters/setters to target any CQL type. -- [bug] JAVA-950: Fix Cluster.connect with a case-sensitive keyspace. -- [bug] JAVA-953: Fix MaterializedViewMetadata when base table name is case sensitive. - - -### 3.0.0-alpha3 - -- [new feature] JAVA-571: Support new system tables in C* 3.0. -- [improvement] JAVA-919: Move crc_check_chance out of compressions options. - -Merged from 2.0 branch: - -- [improvement] JAVA-718: Log streamid at the trace level on sending request and receiving response. -- [bug] JAVA-796: Fix SpeculativeExecutionPolicy.init() and close() are never called. -- [improvement] JAVA-710: Suppress unnecessary warning at shutdown. -- [improvement] #340: Allow DNS name with multiple A-records as contact point. -- [bug] JAVA-794: Allow tracing across multiple result pages. -- [bug] JAVA-737: DowngradingConsistencyRetryPolicy ignores write timeouts. -- [bug] JAVA-736: Forbid bind marker in QueryBuilder add/append/prepend. -- [bug] JAVA-712: Prevent QueryBuilder.quote() from applying duplicate double quotes. -- [bug] JAVA-688: Prevent QueryBuilder from trying to serialize raw string. -- [bug] JAVA-679: Support bind marker in QueryBuilder DELETE's list index. -- [improvement] JAVA-475: Improve QueryBuilder API for SELECT DISTINCT. -- [improvement] JAVA-225: Create values() function for Insert builder using List. -- [improvement] JAVA-702: Warn when ReplicationStrategy encounters invalid - replication factors. -- [improvement] JAVA-662: Add PoolingOptions method to set both core and max - connections. -- [improvement] JAVA-766: Do not include epoll JAR in binary distribution. -- [improvement] JAVA-726: Optimize internal copies of Request objects. -- [bug] JAVA-815: Preserve tracing across retries. -- [improvement] JAVA-709: New RetryDecision.tryNextHost(). -- [bug] JAVA-733: Handle function calls and raw strings as non-idempotent in QueryBuilder. -- [improvement] JAVA-765: Provide API to retrieve values of a Parameterized SimpleStatement. -- [improvement] JAVA-827: implement UPDATE .. IF EXISTS in QueryBuilder. -- [improvement] JAVA-618: Randomize contact points list to prevent hotspots. -- [improvement] JAVA-720: Surface the coordinator used on query failure. -- [bug] JAVA-792: Handle contact points removed during init. -- [improvement] JAVA-719: Allow PlainTextAuthProvider to change its credentials at runtime. -- [new feature] JAVA-151: Make it possible to register for SchemaChange Events. -- [improvement] JAVA-861: Downgrade "Asked to rebuild table" log from ERROR to INFO level. -- [improvement] JAVA-797: Provide an option to prepare statements only on one node. -- [improvement] JAVA-658: Provide an option to not re-prepare all statements in onUp. -- [improvement] JAVA-853: Customizable creation of netty timer. -- [bug] JAVA-859: Avoid quadratic ring processing with invalid replication factors. -- [improvement] JAVA-657: Debounce control connection queries. -- [bug] JAVA-784: LoadBalancingPolicy.distance() called before init(). -- [new feature] JAVA-828: Make driver-side metadata optional. -- [improvement] JAVA-544: Allow hosts to remain partially up. -- [improvement] JAVA-821, JAVA-822: Remove internal blocking calls and expose async session - creation. -- [improvement] JAVA-725: Use parallel calls when re-preparing statement on other - hosts. -- [bug] JAVA-629: Don't use connection timeout for unrelated internal queries. -- [bug] JAVA-892: Fix NPE in speculative executions when metrics disabled. - - -### 3.0.0-alpha2 - -- [new feature] JAVA-875, JAVA-882: Move secondary index metadata out of column definitions. - -Merged from 2.2 branch: - -- [bug] JAVA-847: Propagate CodecRegistry to nested UDTs. -- [improvement] JAVA-848: Ability to store a default, shareable CodecRegistry - instance. -- [bug] JAVA-880: Treat empty ByteBuffers as empty values in TupleCodec and - UDTCodec. - - -### 3.0.0-alpha1 - -- [new feature] JAVA-876: Support new system tables in C* 3.0.0-alpha1. - -Merged from 2.2 branch: - -- [improvement] JAVA-810: Rename DateWithoutTime to LocalDate. -- [bug] JAVA-816: DateCodec does not format values correctly. -- [bug] JAVA-817: TimeCodec does not format values correctly. -- [bug] JAVA-818: TypeCodec.getDataTypeFor() does not handle LocalDate instances. -- [improvement] JAVA-836: Make ResultSet#fetchMoreResult return a - ListenableFuture. -- [improvement] JAVA-843: Disable frozen checks in mapper. -- [improvement] JAVA-721: Allow user to register custom type codecs. -- [improvement] JAVA-722: Support custom type codecs in mapper. - - -### 2.2.0-rc3 - -- [bug] JAVA-847: Propagate CodecRegistry to nested UDTs. -- [improvement] JAVA-848: Ability to store a default, shareable CodecRegistry - instance. -- [bug] JAVA-880: Treat empty ByteBuffers as empty values in TupleCodec and - UDTCodec. - - -### 2.2.0-rc2 - -- [improvement] JAVA-810: Rename DateWithoutTime to LocalDate. -- [bug] JAVA-816: DateCodec does not format values correctly. -- [bug] JAVA-817: TimeCodec does not format values correctly. -- [bug] JAVA-818: TypeCodec.getDataTypeFor() does not handle LocalDate instances. -- [improvement] JAVA-836: Make ResultSet#fetchMoreResult return a - ListenableFuture. -- [improvement] JAVA-843: Disable frozen checks in mapper. -- [improvement] JAVA-721: Allow user to register custom type codecs. -- [improvement] JAVA-722: Support custom type codecs in mapper. - -Merged from 2.1 branch: - -- [bug] JAVA-834: Special case check for 'null' string in index_options column. -- [improvement] JAVA-835: Allow accessor methods with less parameters in case - named bind markers are repeated. -- [improvement] JAVA-475: Improve QueryBuilder API for SELECT DISTINCT. -- [improvement] JAVA-715: Make NativeColumnType a top-level class. -- [improvement] JAVA-700: Expose ProtocolVersion#toInt. -- [bug] JAVA-542: Handle void return types in accessors. -- [improvement] JAVA-225: Create values() function for Insert builder using List. -- [improvement] JAVA-713: HashMap throws an OOM Exception when logging level is set to TRACE. -- [bug] JAVA-679: Support bind marker in QueryBuilder DELETE's list index. -- [improvement] JAVA-732: Expose KEYS and FULL indexing options in IndexMetadata. -- [improvement] JAVA-589: Allow @Enumerated in Accessor method parameters. -- [improvement] JAVA-554: Allow access to table metadata from Mapper. -- [improvement] JAVA-661: Provide a way to map computed fields. -- [improvement] JAVA-824: Ignore missing columns in mapper. -- [bug] JAVA-724: Preserve default timestamp for retries and speculative executions. -- [improvement] JAVA-738: Use same pool implementation for protocol v2 and v3. -- [improvement] JAVA-677: Support CONTAINS / CONTAINS KEY in QueryBuilder. -- [improvement] JAVA-477/JAVA-540: Add USING options in mapper for delete and save - operations. -- [improvement] JAVA-473: Add mapper option to configure whether to save null fields. - -Merged from 2.0 branch: - -- [bug] JAVA-737: DowngradingConsistencyRetryPolicy ignores write timeouts. -- [bug] JAVA-736: Forbid bind marker in QueryBuilder add/append/prepend. -- [bug] JAVA-712: Prevent QueryBuilder.quote() from applying duplicate double quotes. -- [bug] JAVA-688: Prevent QueryBuilder from trying to serialize raw string. -- [bug] JAVA-679: Support bind marker in QueryBuilder DELETE's list index. -- [improvement] JAVA-475: Improve QueryBuilder API for SELECT DISTINCT. -- [improvement] JAVA-225: Create values() function for Insert builder using List. -- [improvement] JAVA-702: Warn when ReplicationStrategy encounters invalid - replication factors. -- [improvement] JAVA-662: Add PoolingOptions method to set both core and max - connections. -- [improvement] JAVA-766: Do not include epoll JAR in binary distribution. -- [improvement] JAVA-726: Optimize internal copies of Request objects. -- [bug] JAVA-815: Preserve tracing across retries. -- [improvement] JAVA-709: New RetryDecision.tryNextHost(). -- [bug] JAVA-733: Handle function calls and raw strings as non-idempotent in QueryBuilder. - - -### 2.2.0-rc1 - -- [new feature] JAVA-783: Protocol V4 enum support. -- [new feature] JAVA-776: Use PK columns in protocol v4 PREPARED response. -- [new feature] JAVA-777: Distinguish NULL and UNSET values. -- [new feature] JAVA-779: Add k/v payload for 3rd party usage. -- [new feature] JAVA-780: Expose server-side warnings on ExecutionInfo. -- [new feature] JAVA-749: Expose new read/write failure exceptions. -- [new feature] JAVA-747: Expose function and aggregate metadata. -- [new feature] JAVA-778: Add new client exception for CQL function failure. -- [improvement] JAVA-700: Expose ProtocolVersion#toInt. -- [new feature] JAVA-404: Support new C* 2.2 CQL date and time types. - -Merged from 2.1 branch: - -- [improvement] JAVA-782: Unify "Target" enum for schema elements. - - -### 2.1.10.2 - -Merged from 2.0 branch: - -- [bug] JAVA-1179: Request objects should be copied when executed. -- [improvement] JAVA-1182: Throw error when synchronous call made on I/O thread. -- [bug] JAVA-1184: Unwrap StatementWrappers when extracting column definitions. - - -### 2.1.10.1 - -- [bug] JAVA-1152: Fix NPE at ControlConnection.refreshNodeListAndTokenMap(). -- [bug] JAVA-1156: Fix NPE at TableMetadata.equals(). - - -### 2.1.10 - -- [bug] JAVA-988: Metadata.handleId should handle escaped double quotes. -- [bug] JAVA-983: QueryBuilder cannot handle collections containing function calls. -- [improvement] JAVA-863: Idempotence propagation in PreparedStatements. -- [bug] JAVA-937: TypeCodec static initializers not always correctly executed. -- [improvement] JAVA-989: Include keyspace name when invalid replication found when generating token map. -- [improvement] JAVA-664: Reduce heap consumption for TokenMap. -- [improvement] JAVA-1030: Log token to replica map computation times. -- [bug] JAVA-1039: Minor bugs in Event Debouncer. -- [improvement] JAVA-843: Disable frozen checks in mapper. -- [improvement] JAVA-833: Improve message when a nested type can't be serialized. -- [improvement] JAVA-1011: Expose PoolingOptions default values. -- [improvement] JAVA-630: Don't process DOWN events for nodes that have active connections. -- [improvement] JAVA-851: Improve UUIDs javadoc with regard to user-provided timestamps. -- [improvement] JAVA-979: Update javadoc for RegularStatement toString() and getQueryString() to indicate that consistency level and other parameters are not maintained in the query string. -- [improvement] JAVA-1038: Fetch node info by rpc_address if its broadcast_address is not in system.peers. -- [improvement] JAVA-974: Validate accessor parameter types against bound statement. -- [bug] JAVA-1068: Unwrap StatementWrappers when hashing the paging state. -- [bug] JAVA-831: Mapper can't load an entity where the PK is a UDT. -- [improvement] JAVA-1021: Improve error message when connect() is called with an invalid keyspace name. -- [improvement] JAVA-879: Mapper.map() accepts mapper-generated and user queries. -- [bug] JAVA-1100: Exception when connecting with shaded java driver in OSGI -- [bug] JAVA-819: Expose more errors in RetryPolicy + provide idempotent-aware wrapper. -- [improvement] JAVA-1040: SimpleStatement parameters support in QueryLogger. -- [bug] JAVA-1064: getTable create statement doesn't properly handle quotes in primary key. -- [improvement] JAVA-888: Add cluster-wide percentile tracker. -- [improvement] JAVA-963: Automatically register PercentileTracker from components that use it. -- [bug] JAVA-1089: Set LWT made from BuiltStatements to non-idempotent. -- [improvement] JAVA-923: Position idempotent flag on object mapper queries. -- [new feature] JAVA-1019: SchemaBuilder support for CREATE/ALTER/DROP KEYSPACE. -- [bug] JAVA-1070: The Mapper should not prepare queries synchronously. -- [new feature] JAVA-982: Introduce new method ConsistencyLevel.isSerial(). -- [bug] JAVA-764: Retry with the normal consistency level (not the serial one) when a write times out on the Paxos phase. -- [bug] JAVA-727: Allow monotonic timestamp generators to drift in the future + use microsecond precision when possible. -- [improvement] JAVA-444: Add Java process information to UUIDs.makeNode() hash. -- [improvement] JAVA-977: Preserve original cause when BuiltStatement value can't be serialized. -- [bug] JAVA-1094: Backport TypeCodec parse and format fixes from 3.0. -- [improvement] JAVA-852: Ignore peers with null entries during discovery. -- [bug] JAVA-1132: Executing bound statement with no variables results in exception with protocol v1. -- [bug] JAVA-1005: DowngradingConsistencyRetryPolicy does not work with EACH_QUORUM when 1 DC is down. -- [bug] JAVA-1002: Avoid deadlock when re-preparing a statement on other hosts. - -Merged from 2.0 branch: - -- [bug] JAVA-994: Don't call on(Up|Down|Add|Remove) methods if Cluster is closed/closing. -- [improvement] JAVA-805: Document that metrics are null until Cluster is initialized. -- [bug] JAVA-1072: Ensure defunct connections are properly evicted from the pool. - - -### 2.1.9 - -- [bug] JAVA-942: Fix implementation of UserType.hashCode(). -- [bug] JAVA-854: avoid early return in Cluster.init when a node doesn't support the protocol version. -- [bug] JAVA-978: Fix quoting issue that caused Mapper.getTableMetadata() to return null. - -Merged from 2.0 branch: - -- [bug] JAVA-950: Fix Cluster.connect with a case-sensitive keyspace. -- [improvement] JAVA-920: Downgrade "error creating pool" message to WARN. -- [bug] JAVA-954: Don't trigger reconnection before initialization complete. -- [improvement] JAVA-914: Avoid rejected tasks at shutdown. -- [improvement] JAVA-921: Add SimpleStatement.getValuesCount(). -- [bug] JAVA-901: Move call to connection.release() out of cancelHandler. -- [bug] JAVA-960: Avoid race in control connection shutdown. -- [bug] JAVA-656: Fix NPE in ControlConnection.updateLocationInfo. -- [bug] JAVA-966: Count uninitialized connections in conviction policy. -- [improvement] JAVA-917: Document SSL configuration. -- [improvement] JAVA-652: Add DCAwareRoundRobinPolicy builder. -- [improvement] JAVA-808: Add generic filtering policy that can be used to exclude specific DCs. - - -### 2.1.8 - -Merged from 2.0 branch: - -- [improvement] JAVA-718: Log streamid at the trace level on sending request and receiving response. - -- [bug] JAVA-796: Fix SpeculativeExecutionPolicy.init() and close() are never called. -- [improvement] JAVA-710: Suppress unnecessary warning at shutdown. -- [improvement] #340: Allow DNS name with multiple A-records as contact point. -- [bug] JAVA-794: Allow tracing across multiple result pages. -- [bug] JAVA-737: DowngradingConsistencyRetryPolicy ignores write timeouts. -- [bug] JAVA-736: Forbid bind marker in QueryBuilder add/append/prepend. -- [bug] JAVA-712: Prevent QueryBuilder.quote() from applying duplicate double quotes. -- [bug] JAVA-688: Prevent QueryBuilder from trying to serialize raw string. -- [bug] JAVA-679: Support bind marker in QueryBuilder DELETE's list index. -- [improvement] JAVA-475: Improve QueryBuilder API for SELECT DISTINCT. -- [improvement] JAVA-225: Create values() function for Insert builder using List. -- [improvement] JAVA-702: Warn when ReplicationStrategy encounters invalid - replication factors. -- [improvement] JAVA-662: Add PoolingOptions method to set both core and max - connections. -- [improvement] JAVA-766: Do not include epoll JAR in binary distribution. -- [improvement] JAVA-726: Optimize internal copies of Request objects. -- [bug] JAVA-815: Preserve tracing across retries. -- [improvement] JAVA-709: New RetryDecision.tryNextHost(). -- [bug] JAVA-733: Handle function calls and raw strings as non-idempotent in QueryBuilder. -- [improvement] JAVA-765: Provide API to retrieve values of a Parameterized SimpleStatement. -- [improvement] JAVA-827: implement UPDATE .. IF EXISTS in QueryBuilder. -- [improvement] JAVA-618: Randomize contact points list to prevent hotspots. -- [improvement] JAVA-720: Surface the coordinator used on query failure. -- [bug] JAVA-792: Handle contact points removed during init. -- [improvement] JAVA-719: Allow PlainTextAuthProvider to change its credentials at runtime. -- [new feature] JAVA-151: Make it possible to register for SchemaChange Events. -- [improvement] JAVA-861: Downgrade "Asked to rebuild table" log from ERROR to INFO level. -- [improvement] JAVA-797: Provide an option to prepare statements only on one node. -- [improvement] JAVA-658: Provide an option to not re-prepare all statements in onUp. -- [improvement] JAVA-853: Customizable creation of netty timer. -- [bug] JAVA-859: Avoid quadratic ring processing with invalid replication factors. -- [improvement] JAVA-657: Debounce control connection queries. -- [bug] JAVA-784: LoadBalancingPolicy.distance() called before init(). -- [new feature] JAVA-828: Make driver-side metadata optional. -- [improvement] JAVA-544: Allow hosts to remain partially up. -- [improvement] JAVA-821, JAVA-822: Remove internal blocking calls and expose async session - creation. -- [improvement] JAVA-725: Use parallel calls when re-preparing statement on other - hosts. -- [bug] JAVA-629: Don't use connection timeout for unrelated internal queries. -- [bug] JAVA-892: Fix NPE in speculative executions when metrics disabled. - - -### 2.1.7.1 - -- [bug] JAVA-834: Special case check for 'null' string in index_options column. -- [improvement] JAVA-835: Allow accessor methods with less parameters in case - named bind markers are repeated. - - -### 2.1.7 - -- [improvement] JAVA-475: Improve QueryBuilder API for SELECT DISTINCT. -- [improvement] JAVA-715: Make NativeColumnType a top-level class. -- [improvement] JAVA-782: Unify "Target" enum for schema elements. -- [improvement] JAVA-700: Expose ProtocolVersion#toInt. -- [bug] JAVA-542: Handle void return types in accessors. -- [improvement] JAVA-225: Create values() function for Insert builder using List. -- [improvement] JAVA-713: HashMap throws an OOM Exception when logging level is set to TRACE. -- [bug] JAVA-679: Support bind marker in QueryBuilder DELETE's list index. -- [improvement] JAVA-732: Expose KEYS and FULL indexing options in IndexMetadata. -- [improvement] JAVA-589: Allow @Enumerated in Accessor method parameters. -- [improvement] JAVA-554: Allow access to table metadata from Mapper. -- [improvement] JAVA-661: Provide a way to map computed fields. -- [improvement] JAVA-824: Ignore missing columns in mapper. -- [bug] JAVA-724: Preserve default timestamp for retries and speculative executions. -- [improvement] JAVA-738: Use same pool implementation for protocol v2 and v3. -- [improvement] JAVA-677: Support CONTAINS / CONTAINS KEY in QueryBuilder. -- [improvement] JAVA-477/JAVA-540: Add USING options in mapper for delete and save - operations. -- [improvement] JAVA-473: Add mapper option to configure whether to save null fields. - -Merged from 2.0 branch: - -- [bug] JAVA-737: DowngradingConsistencyRetryPolicy ignores write timeouts. -- [bug] JAVA-736: Forbid bind marker in QueryBuilder add/append/prepend. -- [bug] JAVA-712: Prevent QueryBuilder.quote() from applying duplicate double quotes. -- [bug] JAVA-688: Prevent QueryBuilder from trying to serialize raw string. -- [bug] JAVA-679: Support bind marker in QueryBuilder DELETE's list index. -- [improvement] JAVA-475: Improve QueryBuilder API for SELECT DISTINCT. -- [improvement] JAVA-225: Create values() function for Insert builder using List. -- [improvement] JAVA-702: Warn when ReplicationStrategy encounters invalid - replication factors. -- [improvement] JAVA-662: Add PoolingOptions method to set both core and max - connections. -- [improvement] JAVA-766: Do not include epoll JAR in binary distribution. -- [improvement] JAVA-726: Optimize internal copies of Request objects. -- [bug] JAVA-815: Preserve tracing across retries. -- [improvement] JAVA-709: New RetryDecision.tryNextHost(). -- [bug] JAVA-733: Handle function calls and raw strings as non-idempotent in QueryBuilder. - - -### 2.1.6 - -Merged from 2.0 branch: - -- [new feature] JAVA-584: Add getObject to BoundStatement and Row. -- [improvement] JAVA-419: Improve connection pool resizing algorithm. -- [bug] JAVA-599: Fix race condition between pool expansion and shutdown. -- [improvement] JAVA-622: Upgrade Netty to 4.0.27. -- [improvement] JAVA-562: Coalesce frames before flushing them to the connection. -- [improvement] JAVA-583: Rename threads to indicate that they are for the driver. -- [new feature] JAVA-550: Expose paging state. -- [new feature] JAVA-646: Slow Query Logger. -- [improvement] JAVA-698: Exclude some errors from measurements in LatencyAwarePolicy. -- [bug] JAVA-641: Fix issue when executing a PreparedStatement from another cluster. -- [improvement] JAVA-534: Log keyspace xxx does not exist at WARN level. -- [improvement] JAVA-619: Allow Cluster subclasses to delegate to another instance. -- [new feature] JAVA-669: Expose an API to check for schema agreement after a - schema-altering statement. -- [improvement] JAVA-692: Make connection and pool creation fully async. -- [improvement] JAVA-505: Optimize connection use after reconnection. -- [improvement] JAVA-617: Remove "suspected" mechanism. -- [improvement] reverts JAVA-425: Don't mark connection defunct on client timeout. -- [new feature] JAVA-561: Speculative query executions. -- [bug] JAVA-666: Release connection before completing the ResultSetFuture. -- [new feature BETA] JAVA-723: Percentile-based variant of query logger and speculative - executions. -- [bug] JAVA-734: Fix buffer leaks when compression is enabled. -- [improvement] JAVA-756: Use Netty's pooled ByteBufAllocator by default. -- [improvement] JAVA-759: Expose "unsafe" paging state API. -- [bug] JAVA-768: Prevent race during pool initialization. - - -### 2.1.5 - -- [bug] JAVA-575: Authorize Null parameter in Accessor method. -- [improvement] JAVA-570: Support C* 2.1.3's nested collections. -- [bug] JAVA-612: Fix checks on mapped collection types. -- [bug] JAVA-672: Fix QueryBuilder.putAll() when the collection contains UDTs. - -Merged from 2.0 branch: - -- [new feature] JAVA-518: Add AddressTranslater for EC2 multi-region deployment. -- [improvement] JAVA-533: Add connection heartbeat. -- [improvement] JAVA-568: Reduce level of logs on missing rpc_address. -- [improvement] JAVA-312, JAVA-681: Expose node token and range information. -- [bug] JAVA-595: Fix cluster name mismatch check at startup. -- [bug] JAVA-620: Fix guava dependency when using OSGI. -- [bug] JAVA-678: Fix handling of DROP events when ks name is case-sensitive. -- [improvement] JAVA-631: Use List instead of List in QueryBuilder API. -- [improvement] JAVA-654: Exclude Netty POM from META-INF in shaded JAR. -- [bug] JAVA-655: Quote single quotes contained in table comments in asCQLQuery method. -- [bug] JAVA-684: Empty TokenRange returned in a one token cluster. -- [improvement] JAVA-687: Expose TokenRange#contains. -- [bug] JAVA-614: Prevent race between cancellation and query completion. -- [bug] JAVA-632: Prevent cancel and timeout from cancelling unrelated ResponseHandler if - streamId was already released and reused. -- [bug] JAVA-642: Fix issue when newly opened pool fails before we could mark the node UP. -- [bug] JAVA-613: Fix unwanted LBP notifications when a contact host is down. -- [bug] JAVA-651: Fix edge cases where a connection was released twice. -- [bug] JAVA-653: Fix edge cases in query cancellation. - - -### 2.1.4 - -Merged from 2.0 branch: - -- [improvement] JAVA-538: Shade Netty dependency. -- [improvement] JAVA-543: Target schema refreshes more precisely. -- [bug] JAVA-546: Don't check rpc_address for control host. -- [improvement] JAVA-409: Improve message of NoHostAvailableException. -- [bug] JAVA-556: Rework connection reaper to avoid deadlock. -- [bug] JAVA-557: Avoid deadlock when multiple connections to the same host get write - errors. -- [improvement] JAVA-504: Make shuffle=true the default for TokenAwarePolicy. -- [bug] JAVA-577: Fix bug when SUSPECT reconnection succeeds, but one of the pooled - connections fails while bringing the node back up. -- [bug] JAVA-419: JAVA-587: Prevent faulty control connection from ignoring reconnecting hosts. -- temporarily revert "Add idle timeout to the connection pool". -- [bug] JAVA-593: Ensure updateCreatedPools does not add pools for suspected hosts. -- [bug] JAVA-594: Ensure state change notifications for a given host are handled serially. -- [bug] JAVA-597: Ensure control connection reconnects when control host is removed. - - -### 2.1.3 - -- [bug] JAVA-510: Ignore static fields in mapper. -- [bug] JAVA-509: Fix UDT parsing at init when using the default protocol version. -- [bug] JAVA-495: Fix toString, equals and hashCode on accessor proxies. -- [bug] JAVA-528: Allow empty name on Column and Field annotations. - -Merged from 2.0 branch: - -- [bug] JAVA-497: Ensure control connection does not trigger concurrent reconnects. -- [improvement] JAVA-472: Keep trying to reconnect on authentication errors. -- [improvement] JAVA-463: Expose close method on load balancing policy. -- [improvement] JAVA-459: Allow load balancing policy to trigger refresh for a single host. -- [bug] JAVA-493: Expose an API to cancel reconnection attempts. -- [bug] JAVA-503: Fix NPE when a connection fails during pool construction. -- [improvement] JAVA-423: Log datacenter name in DCAware policy's init when it is explicitly provided. -- [improvement] JAVA-504: Shuffle the replicas in TokenAwarePolicy.newQueryPlan. -- [improvement] JAVA-507: Make schema agreement wait tuneable. -- [improvement] JAVA-494: Document how to inject the driver metrics into another registry. -- [improvement] JAVA-419: Add idle timeout to the connection pool. -- [bug] JAVA-516: LatencyAwarePolicy does not shutdown executor on invocation of close. -- [improvement] JAVA-451: Throw an exception when DCAwareRoundRobinPolicy is built with - an explicit but null or empty local datacenter. -- [bug] JAVA-511: Fix check for local contact points in DCAware policy's init. -- [improvement] JAVA-457: Make timeout on saturated pool customizable. -- [improvement] JAVA-521: Downgrade Guava to 14.0.1. -- [bug] JAVA-526: Fix token awareness for case-sensitive keyspaces and tables. -- [bug] JAVA-515: Check maximum number of values passed to SimpleStatement. -- [improvement] JAVA-532: Expose the driver version through the API. -- [improvement] JAVA-522: Optimize session initialization when some hosts are not - responsive. - - -### 2.1.2 - -- [improvement] JAVA-361, JAVA-364, JAVA-467: Support for native protocol v3. -- [bug] JAVA-454: Fix UDT fields of type inet in QueryBuilder. -- [bug] JAVA-455: Exclude transient fields from Frozen checks. -- [bug] JAVA-453: Fix handling of null collections in mapper. -- [improvement] JAVA-452: Make implicit column names case-insensitive in mapper. -- [bug] JAVA-433: Fix named bind markers in QueryBuilder. -- [bug] JAVA-458: Fix handling of BigInteger in object mapper. -- [bug] JAVA-465: Ignore synthetic fields in mapper. -- [improvement] JAVA-451: Throw an exception when DCAwareRoundRobinPolicy is built with - an explicit but null or empty local datacenter. -- [improvement] JAVA-469: Add backwards-compatible DataType.serialize methods. -- [bug] JAVA-487: Handle null enum fields in object mapper. -- [bug] JAVA-499: Handle null UDT fields in object mapper. - -Merged from 2.0 branch: - -- [bug] JAVA-449: Handle null pool in PooledConnection.release. -- [improvement] JAVA-425: Defunct connection on request timeout. -- [improvement] JAVA-426: Try next host when we get a SERVER_ERROR. -- [bug] JAVA-449, JAVA-460, JAVA-471: Handle race between query timeout and completion. -- [bug] JAVA-496: Fix DCAwareRoundRobinPolicy datacenter auto-discovery. - - -### 2.1.1 - -- [new] JAVA-441: Support for new "frozen" keyword. - -Merged from 2.0 branch: - -- [bug] JAVA-397: Check cluster name when connecting to a new node. -- [bug] JAVA-326: Add missing CAS delete support in QueryBuilder. -- [bug] JAVA-363: Add collection and data length checks during serialization. -- [improvement] JAVA-329: Surface number of retries in metrics. -- [bug] JAVA-428: Do not use a host when no rpc_address found for it. -- [improvement] JAVA-358: Add ResultSet.wasApplied() for conditional queries. -- [bug] JAVA-349: Fix negative HostConnectionPool open count. -- [improvement] JAVA-436: Log more connection details at trace and debug levels. -- [bug] JAVA-445: Fix cluster shutdown. - - -### 2.1.0 - -- [bug] JAVA-408: ClusteringColumn annotation not working with specified ordering. -- [improvement] JAVA-410: Fail BoundStatement if null values are not set explicitly. -- [bug] JAVA-416: Handle UDT and tuples in BuiltStatement.toString. - -Merged from 2.0 branch: - -- [bug] JAVA-407: Release connections on ResultSetFuture#cancel. -- [bug] JAVA-393: Fix handling of SimpleStatement with values in query builder - batches. -- [bug] JAVA-417: Ensure pool is properly closed in onDown. -- [bug] JAVA-415: Fix tokenMap initialization at startup. -- [bug] JAVA-418: Avoid deadlock on close. - - -### 2.1.0-rc1 - -Merged from 2.0 branch: - -- [bug] JAVA-394: Ensure defunct connections are completely closed. -- [bug] JAVA-342, JAVA-390: Fix memory and resource leak on closed Sessions. - - -### 2.1.0-beta1 - -- [new] Support for User Defined Types and tuples -- [new] Simple object mapper - -Merged from 2.0 branch: everything up to 2.0.3 (included), and the following. - -- [improvement] JAVA-204: Better handling of dead connections. -- [bug] JAVA-373: Fix potential NPE in ControlConnection. -- [bug] JAVA-291: Throws NPE when passed null for a contact point. -- [bug] JAVA-315: Avoid LoadBalancingPolicy onDown+onUp at startup. -- [bug] JAVA-343: Avoid classloader leak in Tomcat. -- [bug] JAVA-387: Avoid deadlock in onAdd/onUp. -- [bug] JAVA-377, JAVA-391: Make metadata parsing more lenient. - - -### 2.0.12.2 - -- [bug] JAVA-1179: Request objects should be copied when executed. -- [improvement] JAVA-1182: Throw error when synchronous call made on I/O thread. -- [bug] JAVA-1184: Unwrap StatementWrappers when extracting column definitions. - - -### 2.0.12.1 - -- [bug] JAVA-994: Don't call on(Up|Down|Add|Remove) methods if Cluster is closed/closing. -- [improvement] JAVA-805: Document that metrics are null until Cluster is initialized. -- [bug] JAVA-1072: Ensure defunct connections are properly evicted from the pool. - - -### 2.0.12 - -- [bug] JAVA-950: Fix Cluster.connect with a case-sensitive keyspace. -- [improvement] JAVA-920: Downgrade "error creating pool" message to WARN. -- [bug] JAVA-954: Don't trigger reconnection before initialization complete. -- [improvement] JAVA-914: Avoid rejected tasks at shutdown. -- [improvement] JAVA-921: Add SimpleStatement.getValuesCount(). -- [bug] JAVA-901: Move call to connection.release() out of cancelHandler. -- [bug] JAVA-960: Avoid race in control connection shutdown. -- [bug] JAVA-656: Fix NPE in ControlConnection.updateLocationInfo. -- [bug] JAVA-966: Count uninitialized connections in conviction policy. -- [improvement] JAVA-917: Document SSL configuration. -- [improvement] JAVA-652: Add DCAwareRoundRobinPolicy builder. -- [improvement] JAVA-808: Add generic filtering policy that can be used to exclude specific DCs. - - -### 2.0.11 - -- [improvement] JAVA-718: Log streamid at the trace level on sending request and receiving response. -- [bug] JAVA-796: Fix SpeculativeExecutionPolicy.init() and close() are never called. -- [improvement] JAVA-710: Suppress unnecessary warning at shutdown. -- [improvement] #340: Allow DNS name with multiple A-records as contact point. -- [bug] JAVA-794: Allow tracing across multiple result pages. -- [bug] JAVA-737: DowngradingConsistencyRetryPolicy ignores write timeouts. -- [bug] JAVA-736: Forbid bind marker in QueryBuilder add/append/prepend. -- [bug] JAVA-712: Prevent QueryBuilder.quote() from applying duplicate double quotes. -- [bug] JAVA-688: Prevent QueryBuilder from trying to serialize raw string. -- [bug] JAVA-679: Support bind marker in QueryBuilder DELETE's list index. -- [improvement] JAVA-475: Improve QueryBuilder API for SELECT DISTINCT. -- [improvement] JAVA-225: Create values() function for Insert builder using List. -- [improvement] JAVA-702: Warn when ReplicationStrategy encounters invalid - replication factors. -- [improvement] JAVA-662: Add PoolingOptions method to set both core and max - connections. -- [improvement] JAVA-766: Do not include epoll JAR in binary distribution. -- [improvement] JAVA-726: Optimize internal copies of Request objects. -- [bug] JAVA-815: Preserve tracing across retries. -- [improvement] JAVA-709: New RetryDecision.tryNextHost(). -- [bug] JAVA-733: Handle function calls and raw strings as non-idempotent in QueryBuilder. -- [improvement] JAVA-765: Provide API to retrieve values of a Parameterized SimpleStatement. -- [improvement] JAVA-827: implement UPDATE .. IF EXISTS in QueryBuilder. -- [improvement] JAVA-618: Randomize contact points list to prevent hotspots. -- [improvement] JAVA-720: Surface the coordinator used on query failure. -- [bug] JAVA-792: Handle contact points removed during init. -- [improvement] JAVA-719: Allow PlainTextAuthProvider to change its credentials at runtime. -- [new feature] JAVA-151: Make it possible to register for SchemaChange Events. -- [improvement] JAVA-861: Downgrade "Asked to rebuild table" log from ERROR to INFO level. -- [improvement] JAVA-797: Provide an option to prepare statements only on one node. -- [improvement] JAVA-658: Provide an option to not re-prepare all statements in onUp. -- [improvement] JAVA-853: Customizable creation of netty timer. -- [bug] JAVA-859: Avoid quadratic ring processing with invalid replication factors. -- [improvement] JAVA-657: Debounce control connection queries. -- [bug] JAVA-784: LoadBalancingPolicy.distance() called before init(). -- [new feature] JAVA-828: Make driver-side metadata optional. -- [improvement] JAVA-544: Allow hosts to remain partially up. -- [improvement] JAVA-821, JAVA-822: Remove internal blocking calls and expose async session - creation. -- [improvement] JAVA-725: Use parallel calls when re-preparing statement on other - hosts. -- [bug] JAVA-629: Don't use connection timeout for unrelated internal queries. -- [bug] JAVA-892: Fix NPE in speculative executions when metrics disabled. - -Merged from 2.0.10_fixes branch: - -- [improvement] JAVA-756: Use Netty's pooled ByteBufAllocator by default. -- [improvement] JAVA-759: Expose "unsafe" paging state API. -- [bug] JAVA-767: Fix getObject by name. -- [bug] JAVA-768: Prevent race during pool initialization. - - -### 2.0.10.1 - -- [improvement] JAVA-756: Use Netty's pooled ByteBufAllocator by default. -- [improvement] JAVA-759: Expose "unsafe" paging state API. -- [bug] JAVA-767: Fix getObject by name. -- [bug] JAVA-768: Prevent race during pool initialization. - - -### 2.0.10 - -- [new feature] JAVA-518: Add AddressTranslater for EC2 multi-region deployment. -- [improvement] JAVA-533: Add connection heartbeat. -- [improvement] JAVA-568: Reduce level of logs on missing rpc_address. -- [improvement] JAVA-312, JAVA-681: Expose node token and range information. -- [bug] JAVA-595: Fix cluster name mismatch check at startup. -- [bug] JAVA-620: Fix guava dependency when using OSGI. -- [bug] JAVA-678: Fix handling of DROP events when ks name is case-sensitive. -- [improvement] JAVA-631: Use List instead of List in QueryBuilder API. -- [improvement] JAVA-654: Exclude Netty POM from META-INF in shaded JAR. -- [bug] JAVA-655: Quote single quotes contained in table comments in asCQLQuery method. -- [bug] JAVA-684: Empty TokenRange returned in a one token cluster. -- [improvement] JAVA-687: Expose TokenRange#contains. -- [new feature] JAVA-547: Expose values of BoundStatement. -- [new feature] JAVA-584: Add getObject to BoundStatement and Row. -- [improvement] JAVA-419: Improve connection pool resizing algorithm. -- [bug] JAVA-599: Fix race condition between pool expansion and shutdown. -- [improvement] JAVA-622: Upgrade Netty to 4.0.27. -- [improvement] JAVA-562: Coalesce frames before flushing them to the connection. -- [improvement] JAVA-583: Rename threads to indicate that they are for the driver. -- [new feature] JAVA-550: Expose paging state. -- [new feature] JAVA-646: Slow Query Logger. -- [improvement] JAVA-698: Exclude some errors from measurements in LatencyAwarePolicy. -- [bug] JAVA-641: Fix issue when executing a PreparedStatement from another cluster. -- [improvement] JAVA-534: Log keyspace xxx does not exist at WARN level. -- [improvement] JAVA-619: Allow Cluster subclasses to delegate to another instance. -- [new feature] JAVA-669: Expose an API to check for schema agreement after a - schema-altering statement. -- [improvement] JAVA-692: Make connection and pool creation fully async. -- [improvement] JAVA-505: Optimize connection use after reconnection. -- [improvement] JAVA-617: Remove "suspected" mechanism. -- [improvement] reverts JAVA-425: Don't mark connection defunct on client timeout. -- [new feature] JAVA-561: Speculative query executions. -- [bug] JAVA-666: Release connection before completing the ResultSetFuture. -- [new feature BETA] JAVA-723: Percentile-based variant of query logger and speculative - executions. -- [bug] JAVA-734: Fix buffer leaks when compression is enabled. - -Merged from 2.0.9_fixes branch: - -- [bug] JAVA-614: Prevent race between cancellation and query completion. -- [bug] JAVA-632: Prevent cancel and timeout from cancelling unrelated ResponseHandler if - streamId was already released and reused. -- [bug] JAVA-642: Fix issue when newly opened pool fails before we could mark the node UP. -- [bug] JAVA-613: Fix unwanted LBP notifications when a contact host is down. -- [bug] JAVA-651: Fix edge cases where a connection was released twice. -- [bug] JAVA-653: Fix edge cases in query cancellation. - - -### 2.0.9.2 - -- [bug] JAVA-651: Fix edge cases where a connection was released twice. -- [bug] JAVA-653: Fix edge cases in query cancellation. - - -### 2.0.9.1 - -- [bug] JAVA-614: Prevent race between cancellation and query completion. -- [bug] JAVA-632: Prevent cancel and timeout from cancelling unrelated ResponseHandler if - streamId was already released and reused. -- [bug] JAVA-642: Fix issue when newly opened pool fails before we could mark the node UP. -- [bug] JAVA-613: Fix unwanted LBP notifications when a contact host is down. - - -### 2.0.9 - -- [improvement] JAVA-538: Shade Netty dependency. -- [improvement] JAVA-543: Target schema refreshes more precisely. -- [bug] JAVA-546: Don't check rpc_address for control host. -- [improvement] JAVA-409: Improve message of NoHostAvailableException. -- [bug] JAVA-556: Rework connection reaper to avoid deadlock. -- [bug] JAVA-557: Avoid deadlock when multiple connections to the same host get write - errors. -- [improvement] JAVA-504: Make shuffle=true the default for TokenAwarePolicy. -- [bug] JAVA-577: Fix bug when SUSPECT reconnection succeeds, but one of the pooled - connections fails while bringing the node back up. -- [bug] JAVA-419: JAVA-587: Prevent faulty control connection from ignoring reconnecting hosts. -- temporarily revert "Add idle timeout to the connection pool". -- [bug] JAVA-593: Ensure updateCreatedPools does not add pools for suspected hosts. -- [bug] JAVA-594: Ensure state change notifications for a given host are handled serially. -- [bug] JAVA-597: Ensure control connection reconnects when control host is removed. - - -### 2.0.8 - -- [bug] JAVA-526: Fix token awareness for case-sensitive keyspaces and tables. -- [bug] JAVA-515: Check maximum number of values passed to SimpleStatement. -- [improvement] JAVA-532: Expose the driver version through the API. -- [improvement] JAVA-522: Optimize session initialization when some hosts are not - responsive. - - -### 2.0.7 - -- [bug] JAVA-449: Handle null pool in PooledConnection.release. -- [improvement] JAVA-425: Defunct connection on request timeout. -- [improvement] JAVA-426: Try next host when we get a SERVER_ERROR. -- [bug] JAVA-449, JAVA-460, JAVA-471: Handle race between query timeout and completion. -- [bug] JAVA-496: Fix DCAwareRoundRobinPolicy datacenter auto-discovery. -- [bug] JAVA-497: Ensure control connection does not trigger concurrent reconnects. -- [improvement] JAVA-472: Keep trying to reconnect on authentication errors. -- [improvement] JAVA-463: Expose close method on load balancing policy. -- [improvement] JAVA-459: Allow load balancing policy to trigger refresh for a single host. -- [bug] JAVA-493: Expose an API to cancel reconnection attempts. -- [bug] JAVA-503: Fix NPE when a connection fails during pool construction. -- [improvement] JAVA-423: Log datacenter name in DCAware policy's init when it is explicitly provided. -- [improvement] JAVA-504: Shuffle the replicas in TokenAwarePolicy.newQueryPlan. -- [improvement] JAVA-507: Make schema agreement wait tuneable. -- [improvement] JAVA-494: Document how to inject the driver metrics into another registry. -- [improvement] JAVA-419: Add idle timeout to the connection pool. -- [bug] JAVA-516: LatencyAwarePolicy does not shutdown executor on invocation of close. -- [improvement] JAVA-451: Throw an exception when DCAwareRoundRobinPolicy is built with - an explicit but null or empty local datacenter. -- [bug] JAVA-511: Fix check for local contact points in DCAware policy's init. -- [improvement] JAVA-457: Make timeout on saturated pool customizable. -- [improvement] JAVA-521: Downgrade Guava to 14.0.1. - - -### 2.0.6 - -- [bug] JAVA-397: Check cluster name when connecting to a new node. -- [bug] JAVA-326: Add missing CAS delete support in QueryBuilder. -- [bug] JAVA-363: Add collection and data length checks during serialization. -- [improvement] JAVA-329: Surface number of retries in metrics. -- [bug] JAVA-428: Do not use a host when no rpc_address found for it. -- [improvement] JAVA-358: Add ResultSet.wasApplied() for conditional queries. -- [bug] JAVA-349: Fix negative HostConnectionPool open count. -- [improvement] JAVA-436: Log more connection details at trace and debug levels. -- [bug] JAVA-445: Fix cluster shutdown. -- [improvement] JAVA-439: Expose child policy in chainable load balancing policies. - - -### 2.0.5 - -- [bug] JAVA-407: Release connections on ResultSetFuture#cancel. -- [bug] JAVA-393: Fix handling of SimpleStatement with values in query builder - batches. -- [bug] JAVA-417: Ensure pool is properly closed in onDown. -- [bug] JAVA-415: Fix tokenMap initialization at startup. -- [bug] JAVA-418: Avoid deadlock on close. - - -### 2.0.4 - -- [improvement] JAVA-204: Better handling of dead connections. -- [bug] JAVA-373: Fix potential NPE in ControlConnection. -- [bug] JAVA-291: Throws NPE when passed null for a contact point. -- [bug] JAVA-315: Avoid LoadBalancingPolicy onDown+onUp at startup. -- [bug] JAVA-343: Avoid classloader leak in Tomcat. -- [bug] JAVA-387: Avoid deadlock in onAdd/onUp. -- [bug] JAVA-377, JAVA-391: Make metadata parsing more lenient. -- [bug] JAVA-394: Ensure defunct connections are completely closed. -- [bug] JAVA-342, JAVA-390: Fix memory and resource leak on closed Sessions. - - -### 2.0.3 - -- [new] The new AbsractSession makes mocking of Session easier. -- [new] JAVA-309: Allow to trigger a refresh of connected hosts. -- [new] JAVA-265: New Session#getState method allows to grab information on - which nodes a session is connected to. -- [new] JAVA-327: Add QueryBuilder syntax for tuples in where clauses (syntax - introduced in Cassandra 2.0.6). -- [improvement] JAVA-359: Properly validate arguments of PoolingOptions methods. -- [bug] JAVA-368: Fix bogus rejection of BigInteger in 'execute with values'. -- [bug] JAVA-367: Signal connection failure sooner to avoid missing them. -- [bug] JAVA-337: Throw UnsupportedOperationException for protocol batch - setSerialCL. - -Merged from 1.0 branch: - -- [bug] JAVA-325: Fix periodic reconnection to down hosts. - - -### 2.0.2 - -- [api] The type of the map key returned by NoHostAvailable#getErrors has changed from - InetAddress to InetSocketAddress. Same for Initializer#getContactPoints return and - for AuthProvider#newAuthenticator. -- [api] JAVA-296: The default load balacing policy is now DCAwareRoundRobinPolicy, and the local - datacenter is automatically picked based on the first connected node. Furthermore, - the TokenAwarePolicy is also used by default. -- [new] JAVA-145: New optional AddressTranslater. -- [bug] JAVA-321: Don't remove quotes on keyspace in the query builder. -- [bug] JAVA-320: Fix potential NPE while cluster undergo schema changes. -- [bug] JAVA-319: Fix thread-safety of page fetching. -- [bug] JAVA-318: Fix potential NPE using fetchMoreResults. - -Merged from 1.0 branch: - -- [new] JAVA-179: Expose the name of the partitioner in use in the cluster metadata. -- [new] Add new WhiteListPolicy to limit the nodes connected to a particular list. -- [improvement] JAVA-289: Do not hop DC for LOCAL_* CL in DCAwareRoundRobinPolicy. -- [bug] JAVA-313: Revert back to longs for dates in the query builder. -- [bug] JAVA-314: Don't reconnect to nodes ignored by the load balancing policy. - - -### 2.0.1 - -- [improvement] JAVA-278: Handle the static columns introduced in Cassandra 2.0.6. -- [improvement] JAVA-208: Add Cluster#newSession method to create Session without connecting - right away. -- [bug] JAVA-279: Add missing iso8601 patterns for parsing dates. -- [bug] Properly parse BytesType as the blob type. -- [bug] JAVA-280: Potential NPE when parsing schema of pre-CQL tables of C* 1.2 nodes. - -Merged from 1.0 branch: - -- [bug] JAVA-275: LatencyAwarePolicy.Builder#withScale doesn't set the scale. -- [new] JAVA-114: Add methods to check if a Cluster/Session instance has been closed already. - - -### 2.0.0 - -- [api] JAVA-269: Case sensitive identifier by default in Metadata. -- [bug] JAVA-274: Fix potential NPE in Cluster#connect. - -Merged from 1.0 branch: - -- [bug] JAVA-263: Always return the PreparedStatement object that is cache internally. -- [bug] JAVA-261: Fix race when multiple connect are done in parallel. -- [bug] JAVA-270: Don't connect at all to nodes that are ignored by the load balancing - policy. - - -### 2.0.0-rc3 - -- [improvement] The protocol version 1 is now supported (features only supported by the - version 2 of the protocol throw UnsupportedFeatureException). -- [improvement] JAVA-195: Make most main objects interface to facilitate testing/mocking. -- [improvement] Adds new getStatements and clear methods to BatchStatement. -- [api] JAVA-247: Renamed shutdown to closeAsync and ShutdownFuture to CloseFuture. Clustering - and Session also now implement Closeable. -- [bug] JAVA-232: Fix potential thread leaks when shutting down Metrics. -- [bug] JAVA-231: Fix potential NPE in HostConnectionPool. -- [bug] JAVA-244: Avoid NPE when node is in an unconfigured DC. -- [bug] JAVA-258: Don't block for scheduled reconnections on Cluster#close. - -Merged from 1.0 branch: - -- [new] JAVA-224: Added Session#prepareAsync calls. -- [new] JAVA-249: Added Cluster#getLoggedKeyspace. -- [improvement] Avoid preparing a statement multiple time per host with multiple sessions. -- [bug] JAVA-255: Make sure connections are returned to the right pools. -- [bug] JAVA-264: Use date string in query build to work-around CASSANDRA-6718. - - -### 2.0.0-rc2 - -- [new] JAVA-207: Add LOCAL_ONE consistency level support (requires using C* 2.0.2+). -- [bug] JAVA-219: Fix parsing of counter types. -- [bug] JAVA-218: Fix missing whitespace for IN clause in the query builder. -- [bug] JAVA-221: Fix replicas computation for token aware balancing. - -Merged from 1.0 branch: - -- [bug] JAVA-213: Fix regression from JAVA-201. -- [improvement] New getter to obtain a snapshot of the scores maintained by - LatencyAwarePolicy. - - -### 2.0.0-rc1 - -- [new] JAVA-199: Mark compression dependencies optional in maven. -- [api] Renamed TableMetadata#getClusteringKey to TableMetadata#getClusteringColumns. - -Merged from 1.0 branch: - -- [new] JAVA-142: OSGi bundle. -- [improvement] JAVA-205: Make collections returned by Row immutable. -- [improvement] JAVA-203: Limit internal thread pool size. -- [bug] JAVA-201: Don't retain unused PreparedStatement in memory. -- [bug] Add missing clustering order info in TableMetadata -- [bug] JAVA-196: Allow bind markers for collections in the query builder. - - -### 2.0.0-beta2 - -- [api] BoundStatement#setX(String, X) methods now set all values (if there is - more than one) having the provided name, not just the first occurence. -- [api] The Authenticator interface now has a onAuthenticationSuccess method that - allows to handle the potential last token sent by the server. -- [new] The query builder don't serialize large values to strings anymore by - default by making use the new ability to send values alongside the query string. -- [new] JAVA-140: The query builder has been updated for new CQL features. -- [bug] Fix exception when a conditional write timeout C* side. -- [bug] JAVA-182: Ensure connection is created when Cluster metadata are asked for. -- [bug] JAVA-187: Fix potential NPE during authentication. - - -### 2.0.0-beta1 - -- [api] The 2.0 version is an API-breaking upgrade of the driver. While most - of the breaking changes are minor, there are too numerous to be listed here - and you are encouraged to look at the Upgrade_guide_to_2.0 file that describe - those changes in details. -- [new] LZ4 compression is supported for the protocol. -- [new] JAVA-39: The driver does not depend on cassandra-all anymore. -- [new] New BatchStatement class allows to execute batch other statements. -- [new] Large ResultSet are now paged (incrementally fetched) by default. -- [new] SimpleStatement support values for bind-variables, to allow - prepare+execute behavior with one roundtrip. -- [new] Query parameters defaults (Consistency level, page size, ...) can be - configured globally. -- [new] New Cassandra 2.0 SERIAL and LOCAL_SERIAL consistency levels are - supported. -- [new] JAVA-116: Cluster#shutdown now waits for ongoing queries to complete by default. -- [new] Generic authentication through SASL is now exposed. -- [bug] JAVA-88: TokenAwarePolicy now takes all replica into account, instead of only the - first one. - - -### 1.0.5 - -- [new] JAVA-142: OSGi bundle. -- [new] JAVA-207: Add support for ConsistencyLevel.LOCAL_ONE; note that this - require Cassandra 1.2.12+. -- [improvement] JAVA-205: Make collections returned by Row immutable. -- [improvement] JAVA-203: Limit internal thread pool size. -- [improvement] New getter to obtain a snapshot of the scores maintained by - LatencyAwarePolicy. -- [improvement] JAVA-222: Avoid synchronization when getting codec for collection - types. -- [bug] JAVA-201, JAVA-213: Don't retain unused PreparedStatement in memory. -- [bug] Add missing clustering order info in TableMetadata -- [bug] JAVA-196: Allow bind markers for collections in the query builder. - - -### 1.0.4 - -- [api] JAVA-163: The Cluster.Builder#poolingOptions and Cluster.Builder#socketOptions - are now deprecated. They are replaced by the new withPoolingOptions and - withSocketOptions methods. -- [new] JAVA-129: A new LatencyAwarePolicy wrapping policy has been added, allowing to - add latency awareness to a wrapped load balancing policy. -- [new] JAVA-161: Cluster.Builder#deferInitialization: Allow defering cluster initialization. -- [new] JAVA-117: Add truncate statement in query builder. -- [new] JAVA-106: Support empty IN in the query builder. -- [bug] JAVA-166: Fix spurious "No current pool set; this should not happen" error - message. -- [bug] JAVA-184: Fix potential overflow in RoundRobinPolicy and correctly errors if - a balancing policy throws. -- [bug] Don't release Stream ID for timeouted queries (unless we do get back - the response) -- [bug] Correctly escape identifiers and use fully qualified table names when - exporting schema as string. - - -### 1.0.3 - -- [api] The query builder now correctly throw an exception when given a value - of a type it doesn't know about. -- [new] SocketOptions#setReadTimeout allows to set a timeout on how long we - wait for the answer of one node. See the javadoc for more details. -- [new] New Session#prepare method that takes a Statement. -- [bug] JAVA-143: Always take per-query CL, tracing, etc. into account for QueryBuilder - statements. -- [bug] Temporary fixup for TimestampType when talking to C* 2.0 nodes. - - -### 1.0.2 - -- [api] Host#getMonitor and all Host.HealthMonitor methods have been - deprecated. The new Host#isUp method is now prefered to the method - in the monitor and you should now register Host.StateListener against - the Cluster object directly (registering against a host HealthMonitor - was much more limited anyway). -- [new] JAVA-92: New serialize/deserialize methods in DataType to serialize/deserialize - values to/from bytes. -- [new] JAVA-128: New getIndexOf() method in ColumnDefinitions to find the index of - a given column name. -- [bug] JAVA-131: Fix a bug when thread could get blocked while setting the current - keyspace. -- [bug] JAVA-136: Quote inet addresses in the query builder since CQL3 requires it. - - -### 1.0.1 - -- [api] JAVA-100: Function call handling in the query builder has been modified in a - backward incompatible way. Function calls are not parsed from string values - anymore as this wasn't safe. Instead the new 'fcall' method should be used. -- [api] Some typos in method names in PoolingOptions have been fixed in a - backward incompatible way before the API get widespread. -- [bug] JAVA-123: Don't destroy composite partition key with BoundStatement and - TokenAwarePolicy. -- [new] null values support in the query builder. -- [new] JAVA-5: SSL support (requires C* >= 1.2.1). -- [new] JAVA-113: Allow generating unlogged batch in the query builder. -- [improvement] Better error message when no host are available. -- [improvement] Improves performance of the stress example application been. - - -### 1.0.0 - -- [api] The AuthInfoProvider has be (temporarily) removed. Instead, the - Cluster builder has a new withCredentials() method to provide a username - and password for use with Cassandra's PasswordAuthenticator. Custom - authenticator will be re-introduced in a future version but are not - supported at the moment. -- [api] The isMetricsEnabled() method in Configuration has been replaced by - getMetricsOptions(). An option to disabled JMX reporting (on by default) - has been added. -- [bug] JAVA-91: Don't make default load balancing policy a static singleton since it - is stateful. - - -### 1.0.0-RC1 - -- [new] JAVA-79: Null values are now supported in BoundStatement (but you will need at - least Cassandra 1.2.3 for it to work). The API of BoundStatement has been - slightly changed so that not binding a variable is not an error anymore, - the variable is simply considered null by default. The isReady() method has - been removed. -- [improvement] JAVA-75: The Cluster/Session shutdown methods now properly block until - the shutdown is complete. A version with at timeout has been added. -- [bug] JAVA-44: Fix use of CQL3 functions in the query builder. -- [bug] JAVA-77: Fix case where multiple schema changes too quickly wouldn't work - (only triggered when 0.0.0.0 was used for the rpc_address on the Cassandra - nodes). -- [bug] JAVA-72: Fix IllegalStateException thrown due to a reconnection made on an I/O - thread. -- [bug] JAVA-82: Correctly reports errors during authentication phase. - - -### 1.0.0-beta2 - -- [new] JAVA-51, JAVA-60, JAVA-58: Support blob constants, BigInteger, BigDecimal and counter batches in - the query builder. -- [new] JAVA-61: Basic support for custom CQL3 types. -- [new] JAVA-65: Add "execution infos" for a result set (this also move the query - trace in the new ExecutionInfos object, so users of beta1 will have to - update). -- [bug] JAVA-62: Fix failover bug in DCAwareRoundRobinPolicy. -- [bug] JAVA-66: Fix use of bind markers for routing keys in the query builder. - - -### 1.0.0-beta1 - -- initial release + + +### 4.0.0 + +- [improvement] JAVA-2192: Don't return generic types with wildcards +- [improvement] JAVA-2148: Add examples +- [bug] JAVA-2189: Exclude virtual keyspaces from token map computation +- [improvement] JAVA-2183: Enable materialized views when testing against Cassandra 4 +- [improvement] JAVA-2182: Add insertInto().json() variant that takes an object in QueryBuilder +- [improvement] JAVA-2161: Annotate mutating methods with `@CheckReturnValue` +- [bug] JAVA-2177: Don't exclude down nodes when initializing LBPs +- [improvement] JAVA-2143: Rename Statement.setTimestamp() to setQueryTimestamp() +- [improvement] JAVA-2165: Abstract node connection information +- [improvement] JAVA-2090: Add support for additional_write_policy and read_repair table options +- [improvement] JAVA-2164: Rename statement builder methods to setXxx +- [bug] JAVA-2178: QueryBuilder: Alias after function column is not included in a query +- [improvement] JAVA-2158: Allow BuildableQuery to build statement with values +- [improvement] JAVA-2150: Improve query builder error message on unsupported literal type +- [documentation] JAVA-2149: Improve Term javadocs in the query builder + +### 4.0.0-rc1 + +- [improvement] JAVA-2106: Log server side warnings returned from a query +- [improvement] JAVA-2151: Drop "Dsl" suffix from query builder main classes +- [new feature] JAVA-2144: Expose internal API to hook into the session lifecycle +- [improvement] JAVA-2119: Add PagingIterable abstraction as a supertype of ResultSet +- [bug] JAVA-2063: Normalize authentication logging +- [documentation] JAVA-2034: Add performance recommendations in the manual +- [improvement] JAVA-2077: Allow reconnection policy to detect first connection attempt +- [improvement] JAVA-2067: Publish javadocs JAR for the shaded module +- [improvement] JAVA-2103: Expose partitioner name in TokenMap API +- [documentation] JAVA-2075: Document preference for LZ4 over Snappy + +### 4.0.0-beta3 + +- [bug] JAVA-2066: Array index range error when fetching routing keys on bound statements +- [documentation] JAVA-2061: Add section to upgrade guide about updated type mappings +- [improvement] JAVA-2038: Add jitter to delays between reconnection attempts +- [improvement] JAVA-2053: Cache results of session.prepare() +- [improvement] JAVA-2058: Make programmatic config reloading part of the public API +- [improvement] JAVA-1943: Fail fast in execute() when the session is closed +- [improvement] JAVA-2056: Reduce HashedWheelTimer tick duration +- [bug] JAVA-2057: Do not create pool when SUGGEST\_UP topology event received +- [improvement] JAVA-2049: Add shorthand method to SessionBuilder to specify local DC +- [bug] JAVA-2037: Fix NPE when preparing statement with no bound variables +- [improvement] JAVA-2014: Schedule timeouts on a separate Timer +- [bug] JAVA-2029: Handle schema refresh failure after a DDL query +- [bug] JAVA-1947: Make schema parsing more lenient and allow missing system_virtual_schema +- [bug] JAVA-2028: Use CQL form when parsing UDT types in system tables +- [improvement] JAVA-1918: Document temporal types +- [improvement] JAVA-1914: Optimize use of System.nanoTime in CqlRequestHandlerBase +- [improvement] JAVA-1945: Document corner cases around UDT and tuple attachment +- [improvement] JAVA-2026: Make CqlDuration implement TemporalAmount +- [improvement] JAVA-2017: Slightly optimize conversion methods on the hot path +- [improvement] JAVA-2010: Make dependencies to annotations required again +- [improvement] JAVA-1978: Add a config option to keep contact points unresolved +- [bug] JAVA-2000: Fix ConcurrentModificationException during channel shutdown +- [improvement] JAVA-2002: Reimplement TypeCodec.accepts to improve performance +- [improvement] JAVA-2011: Re-add ResultSet.getAvailableWithoutFetching() and isFullyFetched() +- [improvement] JAVA-2007: Make driver threads extend FastThreadLocalThread +- [bug] JAVA-2001: Handle zero timeout in admin requests + +### 4.0.0-beta2 + +- [new feature] JAVA-1919: Provide a timestamp <=> ZonedDateTime codec +- [improvement] JAVA-1989: Add BatchStatement.newInstance(BatchType, Iterable) +- [improvement] JAVA-1988: Remove pre-fetching from ResultSet API +- [bug] JAVA-1948: Close session properly when LBP fails to initialize +- [improvement] JAVA-1949: Improve error message when contact points are wrong +- [improvement] JAVA-1956: Add statementsCount accessor to BatchStatementBuilder +- [bug] JAVA-1946: Ignore protocol version in equals comparison for UdtValue/TupleValue +- [new feature] JAVA-1932: Send Driver Name and Version in Startup message +- [new feature] JAVA-1917: Add ability to set node on statement +- [improvement] JAVA-1916: Base TimestampCodec.parse on java.util.Date. +- [improvement] JAVA-1940: Clean up test resources when CCM integration tests finish +- [bug] JAVA-1938: Make CassandraSchemaQueries classes public +- [improvement] JAVA-1925: Rename context getters +- [improvement] JAVA-1544: Check API compatibility with Revapi +- [new feature] JAVA-1900: Add support for virtual tables + +### 4.0.0-beta1 + +- [new feature] JAVA-1869: Add DefaultDriverConfigLoaderBuilder +- [improvement] JAVA-1913: Expose additional counters on Node +- [improvement] JAVA-1880: Rename "config profile" to "execution profile" +- [improvement] JAVA-1889: Upgrade dependencies to the latest minor versions +- [improvement] JAVA-1819: Propagate more attributes to bound statements +- [improvement] JAVA-1897: Improve extensibility of schema metadata classes +- [improvement] JAVA-1437: Enable SSL hostname validation by default +- [improvement] JAVA-1879: Duplicate basic.request options as Request/Statement attributes +- [improvement] JAVA-1870: Use sensible defaults in RequestLogger if config options are missing +- [improvement] JAVA-1877: Use a separate reconnection schedule for the control connection +- [improvement] JAVA-1763: Generate a binary tarball as part of the build process +- [improvement] JAVA-1884: Add additional methods from TypeToken to GenericType +- [improvement] JAVA-1883: Use custom queue implementation for LBP's query plan +- [improvement] JAVA-1890: Add more configuration options to DefaultSslEngineFactory +- [bug] JAVA-1895: Rename PreparedStatement.getPrimaryKeyIndices to getPartitionKeyIndices +- [bug] JAVA-1891: Allow null items when setting values in bulk +- [improvement] JAVA-1767: Improve message when column not in result set +- [improvement] JAVA-1624: Expose ExecutionInfo on exceptions where applicable +- [improvement] JAVA-1766: Revisit nullability +- [new feature] JAVA-1860: Allow reconnection at startup if no contact point is available +- [improvement] JAVA-1866: Make all public policies implement AutoCloseable +- [new feature] JAVA-1762: Build alternate core artifact with Netty shaded +- [new feature] JAVA-1761: Add OSGi descriptors +- [bug] JAVA-1560: Correctly propagate policy initialization errors +- [improvement] JAVA-1865: Add RelationMetadata.getPrimaryKey() +- [improvement] JAVA-1862: Add ConsistencyLevel.isDcLocal and isSerial +- [improvement] JAVA-1858: Implement Serializable in implementations, not interfaces +- [improvement] JAVA-1830: Surface response frame size in ExecutionInfo +- [improvement] JAVA-1853: Add newValue(Object...) to TupleType and UserDefinedType +- [improvement] JAVA-1815: Reorganize configuration into basic/advanced categories +- [improvement] JAVA-1848: Add logs to DefaultRetryPolicy +- [new feature] JAVA-1832: Add Ec2MultiRegionAddressTranslator +- [improvement] JAVA-1825: Add remaining Typesafe config primitive types to DriverConfigProfile +- [new feature] JAVA-1846: Add ConstantReconnectionPolicy +- [improvement] JAVA-1824: Make policies overridable in profiles +- [bug] JAVA-1569: Allow null to be used in positional and named values in statements +- [new feature] JAVA-1592: Expose request's total Frame size through API +- [new feature] JAVA-1829: Add metrics for bytes-sent and bytes-received +- [improvement] JAVA-1755: Normalize usage of DEBUG/TRACE log levels +- [improvement] JAVA-1803: Log driver version on first use +- [improvement] JAVA-1792: Add AuthProvider callback to handle missing challenge from server +- [improvement] JAVA-1775: Assume default packages for built-in policies +- [improvement] JAVA-1774: Standardize policy locations +- [improvement] JAVA-1798: Allow passing the default LBP filter as a session builder argument +- [new feature] JAVA-1523: Add query logger +- [improvement] JAVA-1801: Revisit NodeStateListener and SchemaChangeListener APIs +- [improvement] JAVA-1759: Revisit metrics API +- [improvement] JAVA-1776: Use concurrency annotations +- [improvement] JAVA-1799: Use CqlIdentifier for simple statement named values +- [new feature] JAVA-1515: Add query builder +- [improvement] JAVA-1773: Make DriverConfigProfile enumerable +- [improvement] JAVA-1787: Use standalone shaded Guava artifact +- [improvement] JAVA-1769: Allocate exact buffer size for outgoing requests +- [documentation] JAVA-1780: Add manual section about case sensitivity +- [new feature] JAVA-1536: Add request throttling +- [improvement] JAVA-1772: Revisit multi-response callbacks +- [new feature] JAVA-1537: Add remaining socket options +- [bug] JAVA-1756: Propagate custom payload when preparing a statement +- [improvement] JAVA-1847: Add per-node request tracking + +### 4.0.0-alpha3 + +- [new feature] JAVA-1518: Expose metrics +- [improvement] JAVA-1739: Add host_id and schema_version to node metadata +- [improvement] JAVA-1738: Convert enums to allow extensibility +- [bug] JAVA-1727: Override DefaultUdtValue.equals +- [bug] JAVA-1729: Override DefaultTupleValue.equals +- [improvement] JAVA-1720: Merge Cluster and Session into a single interface +- [improvement] JAVA-1713: Use less nodes in DefaultLoadBalancingPolicyIT +- [improvement] JAVA-1707: Add test infrastructure for running DSE clusters with CCM +- [bug] JAVA-1715: Propagate unchecked exceptions to CompletableFuture in SyncAuthenticator methods +- [improvement] JAVA-1714: Make replication strategies pluggable +- [new feature] JAVA-1647: Handle metadata_changed flag in protocol v5 +- [new feature] JAVA-1633: Handle per-request keyspace in protocol v5 +- [improvement] JAVA-1678: Warn if auth is configured on the client but not the server +- [improvement] JAVA-1673: Remove schema agreement check when repreparing on up +- [new feature] JAVA-1526: Provide a single load balancing policy implementation +- [improvement] JAVA-1680: Improve error message on batch log write timeout +- [improvement] JAVA-1675: Remove dates from copyright headers +- [improvement] JAVA-1645: Don't log stack traces at WARN level +- [new feature] JAVA-1524: Add query trace API +- [improvement] JAVA-1646: Provide a more readable error when connecting to Cassandra 2.0 or lower +- [improvement] JAVA-1662: Raise default request timeout +- [improvement] JAVA-1566: Enforce API rules automatically +- [bug] JAVA-1584: Validate that no bound values are unset in protocol v3 + +### 4.0.0-alpha2 + +- [new feature] JAVA-1525: Handle token metadata +- [new feature] JAVA-1638: Check schema agreement +- [new feature] JAVA-1494: Implement Snappy and LZ4 compression +- [new feature] JAVA-1514: Port Uuids utility class +- [new feature] JAVA-1520: Add node state listeners +- [new feature] JAVA-1493: Handle schema metadata +- [improvement] JAVA-1605: Refactor request execution model +- [improvement] JAVA-1597: Fix raw usages of Statement +- [improvement] JAVA-1542: Enable JaCoCo code coverage +- [improvement] JAVA-1295: Auto-detect best protocol version in mixed cluster +- [bug] JAVA-1565: Mark node down when it loses its last connection and was already reconnecting +- [bug] JAVA-1594: Don't create pool if node comes back up but is ignored +- [bug] JAVA-1593: Reconnect control connection if current node is removed, forced down or ignored +- [bug] JAVA-1595: Don't use system.local.rpc_address when refreshing node list +- [bug] JAVA-1568: Handle Reconnection#reconnectNow/stop while the current attempt is still in + progress +- [improvement] JAVA-1585: Add GenericType#where +- [improvement] JAVA-1590: Properly skip deployment of integration-tests module +- [improvement] JAVA-1576: Expose AsyncResultSet's iterator through a currentPage() method +- [improvement] JAVA-1591: Add programmatic way to get driver version + +### 4.0.0-alpha1 + +- [improvement] JAVA-1586: Throw underlying exception when codec not found in cache +- [bug] JAVA-1583: Handle write failure in ChannelHandlerRequest +- [improvement] JAVA-1541: Reorganize configuration +- [improvement] JAVA-1577: Set default consistency level to LOCAL_ONE +- [bug] JAVA-1548: Retry idempotent statements on READ_TIMEOUT and UNAVAILABLE +- [bug] JAVA-1562: Fix various issues around heart beats +- [improvement] JAVA-1546: Make all statement implementations immutable +- [bug] JAVA-1554: Include VIEW and CDC in WriteType +- [improvement] JAVA-1498: Add a cache above Typesafe config +- [bug] JAVA-1547: Abort pending requests when connection dropped +- [new feature] JAVA-1497: Port timestamp generators from 3.x +- [improvement] JAVA-1539: Configure for deployment to Maven central +- [new feature] JAVA-1519: Close channel if number of orphan stream ids exceeds a configurable + threshold +- [new feature] JAVA-1529: Make configuration reloadable +- [new feature] JAVA-1502: Reprepare statements on newly added/up nodes +- [new feature] JAVA-1530: Add ResultSet.wasApplied +- [improvement] JAVA-1531: Merge CqlSession and Session +- [new feature] JAVA-1513: Handle batch statements +- [improvement] JAVA-1496: Improve log messages +- [new feature] JAVA-1501: Reprepare on the fly when we get an UNPREPARED response +- [bug] JAVA-1499: Wait for load balancing policy at cluster initialization +- [new feature] JAVA-1495: Add prepared statements diff --git a/ci/appveyor.ps1 b/ci/appveyor.ps1 deleted file mode 100644 index bc1d95b69f7..00000000000 --- a/ci/appveyor.ps1 +++ /dev/null @@ -1,132 +0,0 @@ -Add-Type -AssemblyName System.IO.Compression.FileSystem - -$dep_dir="C:\Users\appveyor\deps" -If (!(Test-Path $dep_dir)) { - Write-Host "Creating $($dep_dir)" - New-Item -Path $dep_dir -ItemType Directory -Force -} - -$apr_platform = "Win32" -$openssl_platform = "Win32" -$vc_platform = "x86" -$env:PYTHON="C:\Python27" -$env:OPENSSL_PATH="C:\OpenSSL-Win32" -If ($env:PLATFORM -eq "X64") { - $apr_platform = "x64" - $vc_platform = "x64" - $env:PYTHON="C:\Python27-x64" - $env:OPENSSL_PATH="C:\OpenSSL-Win64" -} - -$env:JAVA_HOME="C:\Program Files\Java\jdk$($env:java_version)" -# The configured java version to test with. -$env:JAVA_PLATFORM_HOME="$($env:JAVA_HOME)" -$env:JAVA_8_HOME="C:\Program Files\Java\jdk1.8.0" -$env:PATH="$($env:PYTHON);$($env:PYTHON)\Scripts;$($env:JAVA_HOME)\bin;$($env:OPENSSL_PATH)\bin;$($env:PATH)" -$env:CCM_PATH="$($dep_dir)\ccm" - -$apr_dist_path = "$($dep_dir)\apr" -# Build APR if it hasn't been previously built. -If (!(Test-Path $apr_dist_path)) { - Write-Host "Cloning APR" - $apr_path = "C:\Users\appveyor\apr" - Start-Process git -ArgumentList "clone --branch=1.5.2 --depth=1 https://github.com/apache/apr.git $($apr_path)" -Wait -nnw - Write-Host "Setting Visual Studio Environment to VS 2015" - Push-Location "$($env:VS140COMNTOOLS)\..\..\VC" - cmd /c "vcvarsall.bat $vc_platform & set" | - foreach { - if ($_ -match "=") { - $v = $_.split("="); Set-Item -force -path "ENV:\$($v[0])" -value "$($v[1])" - } - } - Pop-Location - Write-Host "Building APR (an error may be printed, but it will still build)" - Push-Location $($apr_path) - cmd /c nmake -f Makefile.win ARCH="$apr_platform Release" PREFIX=$($apr_dist_path) buildall install - Pop-Location - Write-Host "Done Building APR" -} -$env:PATH="$($apr_dist_path)\bin;$($env:PATH)" - -# Install Ant and Maven -$ant_base = "$($dep_dir)\ant" -$ant_path = "$($ant_base)\apache-ant-1.9.7" -If (!(Test-Path $ant_path)) { - Write-Host "Installing Ant" - $ant_url = "https://www.dropbox.com/s/lgx95x1jr6s787l/apache-ant-1.9.7-bin.zip?dl=1" - $ant_zip = "C:\Users\appveyor\apache-ant-1.9.7-bin.zip" - (new-object System.Net.WebClient).DownloadFile($ant_url, $ant_zip) - [System.IO.Compression.ZipFile]::ExtractToDirectory($ant_zip, $ant_base) -} -$env:PATH="$($ant_path)\bin;$($env:PATH)" - -$maven_base = "$($dep_dir)\maven" -$maven_path = "$($maven_base)\apache-maven-3.2.5" -If (!(Test-Path $maven_path)) { - Write-Host "Installing Maven" - $maven_url = "https://www.dropbox.com/s/fh9kffmexprsmha/apache-maven-3.2.5-bin.zip?dl=1" - $maven_zip = "C:\Users\appveyor\apache-maven-3.2.5-bin.zip" - (new-object System.Net.WebClient).DownloadFile($maven_url, $maven_zip) - [System.IO.Compression.ZipFile]::ExtractToDirectory($maven_zip, $maven_base) -} -$env:M2_HOME="$($maven_path)" -$env:PATH="$($maven_path)\bin;$($env:PATH)" - -$jdks = @("1.6.0", "1.7.0", "1.8.0") -foreach ($jdk in $jdks) { - $java_dir = "C:\Program Files\Java\jdk$jdk" - $jce_target = "$java_dir\jre\lib\security" - $jce_indicator = "$jce_target\README.txt" - # Install Java Cryptographic Extensions, needed for SSL. - # If this file doesn't exist we know JCE hasn't been installed. - If (!(Test-Path $jce_indicator)) { - Write-Host "Installing JCE for $jdk" - $zip = "$dep_dir\jce_policy-$jdk.zip" - $url = "https://www.dropbox.com/s/po4308hlwulpvep/UnlimitedJCEPolicyJDK7.zip?dl=1" - $extract_folder = "UnlimitedJCEPolicy" - If ($jdk -eq "1.8.0") { - $url = "https://www.dropbox.com/s/al1e6e92cjdv7m7/jce_policy-8.zip?dl=1" - $extract_folder = "UnlimitedJCEPolicyJDK8" - } - ElseIf ($jdk -eq "1.6.0") { - $url = "https://www.dropbox.com/s/dhrtucxcif4n11k/jce_policy-6.zip?dl=1" - $extract_folder = "jce" - } - # Download zip to staging area if it doesn't exist, we do this because - # we extract it to the directory based on the platform and we want to cache - # this file so it can apply to all platforms. - if(!(Test-Path $zip)) { - (new-object System.Net.WebClient).DownloadFile($url, $zip) - } - - [System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $jce_target) - - $jcePolicyDir = "$jce_target\$extract_folder" - Move-Item $jcePolicyDir\* $jce_target\ -force - Remove-Item $jcePolicyDir - } -} - -# Install Python Dependencies for CCM. -Write-Host "Installing Python Dependencies for CCM" -Start-Process python -ArgumentList "-m pip install psutil pyYaml six" -Wait -nnw - -# Clone ccm from git and use master. -If (!(Test-Path $env:CCM_PATH)) { - Write-Host "Cloning CCM" - Start-Process git -ArgumentList "clone https://github.com/pcmanus/ccm.git $($env:CCM_PATH)" -Wait -nnw -} - -# Copy ccm -> ccm.py so windows knows to run it. -If (!(Test-Path $env:CCM_PATH\ccm.py)) { - Copy-Item "$env:CCM_PATH\ccm" "$env:CCM_PATH\ccm.py" -} -$env:PYTHONPATH="$($env:CCM_PATH);$($env:PYTHONPATH)" -$env:PATH="$($env:CCM_PATH);$($env:PATH)" - -# Predownload cassandra version for CCM if it isn't already downloaded. -If (!(Test-Path C:\Users\appveyor\.ccm\repository\$env:cassandra_version)) { - Write-Host "Preinstalling C* $($env:cassandra_version)" - Start-Process python -ArgumentList "$($env:CCM_PATH)\ccm.py create -v $($env:cassandra_version) -n 1 predownload" -Wait -nnw - Start-Process python -ArgumentList "$($env:CCM_PATH)\ccm.py remove predownload" -Wait -nnw -} diff --git a/ci/appveyor.yml b/ci/appveyor.yml deleted file mode 100644 index 81dd5b01958..00000000000 --- a/ci/appveyor.yml +++ /dev/null @@ -1,20 +0,0 @@ -environment: - test_profile: default - matrix: - - java_version: 1.6.0 - - java_version: 1.7.0 - - java_version: 1.8.0 - test_profile: short -platform: x64 -install: - - ps: .\ci\appveyor.ps1 -build_script: - - "set \"JAVA_HOME=%JAVA_8_HOME%\" && mvn install -DskipTests=true -B -V" -test_script: - - "set \"JAVA_HOME=%JAVA_PLATFORM_HOME%\" && mvn -B -D\"ccm.java.home\"=\"%JAVA_8_HOME%\" -D\"ccm.maxNumberOfNodes\"=1 -D\"cassandra.version\"=%cassandra_version% verify -P %test_profile%" -on_finish: - - ps: .\ci\uploadtests.ps1 -cache: - - C:\Users\appveyor\.m2 - - C:\Users\appveyor\.ccm\repository - - C:\Users\appveyor\deps -> .\ci\appveyor.ps1 diff --git a/ci/install-jdk.sh b/ci/install-jdk.sh new file mode 100644 index 00000000000..674961c2daf --- /dev/null +++ b/ci/install-jdk.sh @@ -0,0 +1,318 @@ +#!/usr/bin/env bash + +# +# Install JDK for Linux and Mac OS +# +# This script determines the most recent early-access build number, +# downloads the JDK archive to the user home directory and extracts +# it there. +# +# Exported environment variables (when sourcing this script) +# +# JAVA_HOME is set to the extracted JDK directory +# PATH is prepended with ${JAVA_HOME}/bin +# +# (C) 2018 Christian Stein +# +# https://github.com/sormuras/bach/blob/master/install-jdk.sh +# + +set -o errexit +#set -o nounset # https://github.com/travis-ci/travis-ci/issues/5434 +#set -o xtrace + +function initialize() { + readonly script_name="$(basename "${BASH_SOURCE[0]}")" + readonly script_version='2018-10-17' + + dry=false + silent=false + verbose=false + emit_java_home=false + + feature='ea' + license='GPL' + os='?' + url='?' + workspace="${HOME}" + target='?' + cacerts=false +} + +function usage() { +cat << EOF +Usage: ${script_name} [OPTION]... +Download and extract the latest-and-greatest JDK from java.net or Oracle. + +Version: ${script_version} +Options: + -h|--help Displays this help + -d|--dry-run Activates dry-run mode + -s|--silent Displays no output + -e|--emit-java-home Print value of "JAVA_HOME" to stdout (ignores silent mode) + -v|--verbose Displays verbose output + + -f|--feature 9|10|...|ea JDK feature release number, defaults to "ea" + -l|--license GPL|BCL License defaults to "GPL", BCL also indicates OTN-LA for Oracle Java SE + -o|--os linux-x64|osx-x64 Operating system identifier (works best with GPL license) + -u|--url "https://..." Use custom JDK archive (provided as .tar.gz file) + -w|--workspace PATH Working directory defaults to \${HOME} [${HOME}] + -t|--target PATH Target directory, defaults to first component of the tarball + -c|--cacerts Link system CA certificates (currently only Debian/Ubuntu is supported) +EOF +} + +function script_exit() { + if [[ $# -eq 1 ]]; then + printf '%s\n' "$1" + exit 0 + fi + + if [[ $# -eq 2 && $2 =~ ^[0-9]+$ ]]; then + printf '%b\n' "$1" + exit "$2" + fi + + script_exit 'Invalid arguments passed to script_exit()!' 2 +} + +function say() { + if [[ ${silent} != true ]]; then + echo "$@" + fi +} + +function verbose() { + if [[ ${verbose} == true ]]; then + echo "$@" + fi +} + +function parse_options() { + local option + while [[ $# -gt 0 ]]; do + option="$1" + shift + case ${option} in + -h|-H|--help) + usage + exit 0 + ;; + -v|-V|--verbose) + verbose=true + ;; + -s|-S|--silent) + silent=true + verbose "Silent mode activated" + ;; + -d|-D|--dry-run) + dry=true + verbose "Dry-run mode activated" + ;; + -e|-E|--emit-java-home) + emit_java_home=true + verbose "Emitting JAVA_HOME" + ;; + -f|-F|--feature) + feature="$1" + verbose "feature=${feature}" + shift + ;; + -l|-L|--license) + license="$1" + verbose "license=${license}" + shift + ;; + -o|-O|--os) + os="$1" + verbose "os=${os}" + shift + ;; + -u|-U|--url) + url="$1" + verbose "url=${url}" + shift + ;; + -w|-W|--workspace) + workspace="$1" + verbose "workspace=${workspace}" + shift + ;; + -t|-T|--target) + target="$1" + verbose "target=${target}" + shift + ;; + -c|-C|--cacerts) + cacerts=true + verbose "Linking system CA certificates" + ;; + *) + script_exit "Invalid argument was provided: ${option}" 2 + ;; + esac + done +} + +function determine_latest_jdk() { + local number + local curl_result + local url + + verbose "Determine latest JDK feature release number" + number=9 + while [[ ${number} != 99 ]] + do + url=http://jdk.java.net/${number} + curl_result=$(curl -o /dev/null --silent --head --write-out %{http_code} ${url}) + if [[ ${curl_result} -ge 400 ]]; then + break + fi + verbose " Found ${url} [${curl_result}]" + latest_jdk=${number} + number=$[$number +1] + done + + verbose "Latest JDK feature release number is: ${latest_jdk}" +} + +function perform_sanity_checks() { + if [[ ${feature} == '?' ]] || [[ ${feature} == 'ea' ]]; then + feature=${latest_jdk} + fi + if [[ ${feature} -lt 9 ]] || [[ ${feature} -gt ${latest_jdk} ]]; then + script_exit "Expected feature release number in range of 9 to ${latest_jdk}, but got: ${feature}" 3 + fi + if [[ -d "$target" ]]; then + script_exit "Target directory must not exist, but it does: $(du -hs '${target}')" 3 + fi +} + +function determine_url() { + local DOWNLOAD='https://download.java.net/java' + local ORACLE='http://download.oracle.com/otn-pub/java/jdk' + + # Archived feature or official GA build? + case "${feature}-${license}" in + 9-GPL) url="${DOWNLOAD}/GA/jdk9/9.0.4/binaries/openjdk-9.0.4_${os}_bin.tar.gz"; return;; + 9-BCL) url="${ORACLE}/9.0.4+11/c2514751926b4512b076cc82f959763f/jdk-9.0.4_${os}_bin.tar.gz"; return;; + 10-GPL) url="${DOWNLOAD}/GA/jdk10/10.0.2/19aef61b38124481863b1413dce1855f/13/openjdk-10.0.2_${os}_bin.tar.gz"; return;; + 10-BCL) url="${ORACLE}/10.0.2+13/19aef61b38124481863b1413dce1855f/jdk-10.0.2_${os}_bin.tar.gz"; return;; + 11-GPL) url="${DOWNLOAD}/GA/jdk11/13/GPL/openjdk-11.0.1_${os}_bin.tar.gz"; return;; + 11-BCL) url="${ORACLE}/11.0.1+13/90cf5d8f270a4347a95050320eef3fb7/jdk-11.0.1_${os}_bin.tar.gz"; return;; + esac + + # EA or RC build? + local JAVA_NET="http://jdk.java.net/${feature}" + local candidates=$(wget --quiet --output-document - ${JAVA_NET} | grep -Eo 'href[[:space:]]*=[[:space:]]*"[^\"]+"' | grep -Eo '(http|https)://[^"]+') + url=$(echo "${candidates}" | grep -Eo "${DOWNLOAD}/.+/jdk${feature}/.+/${license}/.*jdk-${feature}.+${os}_bin.tar.gz$" || true) + + if [[ -z ${url} ]]; then + script_exit "Couldn't determine a download url for ${feature}-${license} on ${os}" 1 + fi +} + +function prepare_variables() { + if [[ ${os} == '?' ]]; then + if [[ "$OSTYPE" == "darwin"* ]]; then + os='osx-x64' + else + os='linux-x64' + fi + fi + if [[ ${url} == '?' ]]; then + determine_latest_jdk + perform_sanity_checks + determine_url + else + feature='' + license='' + os='' + fi + archive="${workspace}/$(basename ${url})" + status=$(curl -o /dev/null --silent --head --write-out %{http_code} ${url}) +} + +function print_variables() { +cat << EOF +Variables: + feature = ${feature} + license = ${license} + os = ${os} + url = ${url} + status = ${status} + archive = ${archive} +EOF +} + +function download_and_extract_and_set_target() { + local quiet='--quiet'; if [[ ${verbose} == true ]]; then quiet=''; fi + local local="--directory-prefix ${workspace}" + local remote='--timestamping --continue' + local wget_options="${quiet} ${local} ${remote}" + local tar_options="--file ${archive}" + + say "Downloading JDK from ${url}..." + verbose "Using wget options: ${wget_options}" + if [[ ${license} == 'GPL' ]]; then + wget ${wget_options} ${url} + else + wget ${wget_options} --header "Cookie: oraclelicense=accept-securebackup-cookie" ${url} + fi + + verbose "Using tar options: ${tar_options}" + if [[ ${target} == '?' ]]; then + tar --extract ${tar_options} -C "${workspace}" + if [[ "$OSTYPE" != "darwin"* ]]; then + target="${workspace}"/$(tar --list ${tar_options} | grep 'bin/javac' | tr '/' '\n' | tail -3 | head -1) + else + target="${workspace}"/$(tar --list ${tar_options} | head -2 | tail -1 | cut -f 2 -d '/' -)/Contents/Home + fi + else + if [[ "$OSTYPE" != "darwin"* ]]; then + mkdir --parents "${target}" + tar --extract ${tar_options} -C "${target}" --strip-components=1 + else + mkdir -p "${target}" + tar --extract ${tar_options} -C "${target}" --strip-components=4 # . / / Contents / Home + fi + fi + + if [[ ${verbose} == true ]]; then + echo "Set target to: ${target}" + echo "Content of target directory:" + ls "${target}" + echo "Content of release file:" + [[ ! -f "${target}/release" ]] || cat "${target}/release" + fi + + # Link to system certificates + # http://openjdk.java.net/jeps/319 + # https://bugs.openjdk.java.net/browse/JDK-8196141 + # TODO: Provide support for other distributions than Debian/Ubuntu + if [[ ${cacerts} == true ]]; then + mv "${target}/lib/security/cacerts" "${target}/lib/security/cacerts.jdk" + ln -s /etc/ssl/certs/java/cacerts "${target}/lib/security/cacerts" + fi +} + +function main() { + initialize + say "$script_name $script_version" + + parse_options "$@" + prepare_variables + + if [[ ${silent} == false ]]; then print_variables; fi + if [[ ${dry} == true ]]; then exit 0; fi + + download_and_extract_and_set_target + + export JAVA_HOME=$(cd "${target}"; pwd) + export PATH=${JAVA_HOME}/bin:$PATH + + if [[ ${silent} == false ]]; then java -version; fi + if [[ ${emit_java_home} == true ]]; then echo "${JAVA_HOME}"; fi +} + +main "$@" \ No newline at end of file diff --git a/ci/uploadtests.ps1 b/ci/uploadtests.ps1 deleted file mode 100644 index cf88b16229c..00000000000 --- a/ci/uploadtests.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -$testResults=Get-ChildItem TEST-TestSuite.xml -Recurse - -Write-Host "Uploading test results." - -$url = "https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)" -$wc = New-Object 'System.Net.WebClient' - -foreach ($testResult in $testResults) { - try { - Write-Host -ForegroundColor Green "Uploading $testResult -> $url." - $wc.UploadFile($url, $testResult) - } catch [Net.WebException] { - Write-Host -ForegroundColor Red "Failed Uploading $testResult -> $url. $_" - } -} - -Write-Host "Done uploading test results." diff --git a/clirr-ignores.xml b/clirr-ignores.xml deleted file mode 100644 index 8c1bd47ab4c..00000000000 --- a/clirr-ignores.xml +++ /dev/null @@ -1,354 +0,0 @@ - - - - - 8001 - com/datastax/driver/mapping/ColumnMapper$Kind - False positive, the enclosing class is package-private so this was never exposed - - - - 1001 - com/datastax/driver/mapping/ColumnMapper$Kind - False positive, the enclosing class is package-private so this was never exposed - - - - 7012 - com/datastax/driver/mapping/annotations/QueryParameters - boolean[] idempotent() - False positive, it's an annotation and the new method has a default value - - - - 8001 - com/datastax/driver/extras/codecs/jdk8/InstantCodec - This class is only present if the project was compiled with JDK 8+ - - - - 8001 - com/datastax/driver/extras/codecs/jdk8/LocalDateCodec - This class is only present if the project was compiled with JDK 8+ - - - - 8001 - com/datastax/driver/extras/codecs/jdk8/LocalTimeCodec - This class is only present if the project was compiled with JDK 8+ - - - - 8001 - com/datastax/driver/extras/codecs/jdk8/OptionalCodec - This class is only present if the project was compiled with JDK 8+ - - - - 8001 - com/datastax/driver/extras/codecs/jdk8/ZonedDateTimeCodec - This class is only present if the project was compiled with JDK 8+ - - - - 7002 - com/datastax/driver/core/PerHostPercentileTracker - com.datastax.driver.core.PerHostPercentileTracker$Builder builderWithHighestTrackableLatencyMillis(long) - - Renamed (API was marked as beta and still subject to change) - - - - 7002 - com/datastax/driver/core/PerHostPercentileTracker - long getLatencyAtPercentile(com.datastax.driver.core.Host, double) - Moved to new parent class with more parameters (API was marked as beta and still subject to change) - - - - 7002 - com/datastax/driver/core/PerHostPercentileTracker$Builder - com.datastax.driver.core.PerHostPercentileTracker$Builder withInterval(long, java.util.concurrent.TimeUnit) - False positive, method now inherited from generic parent class - - - - 7002 - com/datastax/driver/core/PerHostPercentileTracker$Builder - com.datastax.driver.core.PerHostPercentileTracker$Builder withMinRecordedValues(int) - False positive, method now inherited from generic parent class - - - - 7002 - com/datastax/driver/core/PerHostPercentileTracker$Builder - com.datastax.driver.core.PerHostPercentileTracker$Builder withNumberOfSignificantValueDigits(int) - False positive, method now inherited from generic parent class - - - - 7002 - com/datastax/driver/core/PerHostPercentileTracker$Builder - com.datastax.driver.core.PerHostPercentileTracker$Builder withNumberOfHosts(int) - Removed (API was marked as beta and still subject to change) - - - - 7005 - com/datastax/driver/core/QueryLogger$Builder - com.datastax.driver.core.QueryLogger$Builder withDynamicThreshold(com.datastax.driver.core.PerHostPercentileTracker, double) - com.datastax.driver.core.QueryLogger$Builder withDynamicThreshold(com.datastax.driver.core.PercentileTracker, double) - Introduced more generic parent type PercentileTracker (API was marked as beta and still subject to change) - - - - 7002 - com/datastax/driver/core/QueryLogger$DynamicThresholdQueryLogger - com.datastax.driver.core.PerHostPercentileTracker getPerHostPercentileLatencyTracker() - Introduced more generic parent type PercentileTracker (API was marked as beta and still subject to change) - - - - 7002 - com/datastax/driver/core/QueryLogger$DynamicThresholdQueryLogger - void setPerHostPercentileLatencyTracker(com.datastax.driver.core.PerHostPercentileTracker) - Introduced more generic parent type PercentileTracker (API was marked as beta and still subject to change) - - - - 7005 - com/datastax/driver/core/policies/PercentileSpeculativeExecutionPolicy - PercentileSpeculativeExecutionPolicy(com.datastax.driver.core.PerHostPercentileTracker, double, int) - * - Introduced more generic parent type PercentileTracker (API was marked as beta and still subject to change) - - - - 8001 - com/datastax/driver/core/FrameCompressor$SnappyCompressor - False positive, the enclosing class is package-private so this was never exposed - - - 8001 - com/datastax/driver/core/FrameCompressor$LZ4Compressor - False positive, the enclosing class is package-private so this was never exposed - - - - 7005 - com/datastax/driver/core/querybuilder/QueryBuilder - - * - * - Relaxed parameters from List to Iterable for in, lt, lte, eq, gt, and gte - - - 7005 - com/datastax/driver/core/exceptions/AlreadyExistsException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/AuthenticationException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/BootstrappingException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/BusyConnectionException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/BusyPoolException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 6001 - com/datastax/driver/core/exceptions/ConnectionException - address - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/ConnectionException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7012 - com/datastax/driver/core/exceptions/CoordinatorException - com.datastax.driver.core.EndPoint getEndPoint() - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/FunctionExecutionException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/InvalidConfigurationInQueryException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/InvalidQueryException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/OperationTimedOutException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/OperationTimedOutException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/OverloadedException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/ProtocolError - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/QueryConsistencyException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/ReadFailureException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/ReadTimeoutException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/ServerError - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/SyntaxError - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/TransportException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/TruncateException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/UnauthorizedException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/UnavailableException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/UnpreparedException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/UnsupportedProtocolVersionException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/WriteFailureException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - - 7005 - com/datastax/driver/core/exceptions/WriteTimeoutException - *java.net.InetSocketAddress* - *com.datastax.driver.core.EndPoint* - JAVA-2355: Abstract connection information into new EndPoint type for sni support - - diff --git a/core-shaded/pom.xml b/core-shaded/pom.xml new file mode 100644 index 00000000000..041da3a3b5f --- /dev/null +++ b/core-shaded/pom.xml @@ -0,0 +1,317 @@ + + + + 4.0.0 + + + com.datastax.oss + java-driver-parent + 4.0.0 + + + java-driver-core-shaded + + DataStax Java driver for Apache Cassandra(R) - core with shaded deps + + + + + com.datastax.oss + java-driver-core + ${project.version} + + + + com.datastax.oss + native-protocol + + + com.datastax.oss + java-driver-shaded-guava + + + com.typesafe + config + + + com.github.jnr + jnr-ffi + + + com.github.jnr + jnr-posix + + + org.xerial.snappy + snappy-java + true + + + org.lz4 + lz4-java + true + + + org.slf4j + slf4j-api + + + io.dropwizard.metrics + metrics-core + + + org.hdrhistogram + HdrHistogram + + + com.github.stephenc.jcip + jcip-annotations + + + com.github.spotbugs + spotbugs-annotations + + + + + + + + + maven-shade-plugin + + + shade-core-dependencies + package + + shade + + + true + true + + + + com.datastax.oss:java-driver-core + io.netty:* + + + + + io.netty + com.datastax.oss.driver.shaded.netty + + + + + + + + maven-dependency-plugin + + + unpack-shaded-classes + package + + unpack + + + + + com.datastax.oss + java-driver-core-shaded + ${project.version} + jar + ${project.build.outputDirectory} + + + + + META-INF/maven/com.datastax.oss/java-driver-core/**, + META-INF/maven/io.netty/**, + + + + + + unpack-shaded-sources + package + + unpack + + + + + com.datastax.oss + java-driver-core-shaded + ${project.version} + jar + sources + ${project.build.directory}/shaded-sources + + + + + + + + maven-javadoc-plugin + + + attach-shaded-javadocs + + jar + + + ${project.build.directory}/shaded-sources + + com.datastax.oss.driver.internal:com.datastax.oss.driver.shaded + + + + + org.jctools + jctools-core + 2.1.2 + + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + generate-shaded-manifest + package + + manifest + + + + com.datastax.oss.driver.core + + * + + + !com.datastax.oss.driver.shaded.netty.*, + !jnr.*, + !net.jcip.annotations.*, + !edu.umd.cs.findbugs.annotations.*, + !com.google.protobuf.*, + !com.jcraft.jzlib.*, + !com.ning.compress.*, + !lzma.sdk.*, + !net.jpountz.xxhash.*, + !org.bouncycastle.*, + !org.conscrypt.*, + !org.apache.commons.logging.*, + !org.apache.log4j.*, + !org.apache.logging.log4j.*, + !org.eclipse.jetty.*, + !org.jboss.marshalling.*, + !sun.misc.*, + !sun.security.*, + * + + + + com.datastax.oss.driver.api.core.*, + com.datastax.oss.driver.internal.core.*, + com.datastax.oss.driver.shaded.netty.*, + + + true + + + + + + maven-assembly-plugin + + + generate-final-shaded-jar + package + + single + + + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + + src/assembly/shaded-jar.xml + + + false + + + + + + org.revapi + revapi-maven-plugin + + true + + + + + diff --git a/core-shaded/src/assembly/shaded-jar.xml b/core-shaded/src/assembly/shaded-jar.xml new file mode 100644 index 00000000000..3a735f36d2a --- /dev/null +++ b/core-shaded/src/assembly/shaded-jar.xml @@ -0,0 +1,44 @@ + + + shaded-jar + + jar + + false + + + + ${project.build.outputDirectory} + + + + + + + ${project.basedir}/dependency-reduced-pom.xml + META-INF/maven/com.datastax.oss/java-driver-core-shaded + pom.xml + + + \ No newline at end of file diff --git a/core/console.scala b/core/console.scala new file mode 100644 index 00000000000..0ae13620ff8 --- /dev/null +++ b/core/console.scala @@ -0,0 +1,39 @@ +/* + * Allows quick manual tests from the Scala console: + * + * cd core/ + * mvn scala:console + * + * The script below is run at init, then you can do `val cluster = builder.build()` and play with + * it. + * + * Note: on MacOS, the Scala plugin seems to break the terminal if you exit the console with `:q`. + * Use Ctrl+C instead. + */ +import com.datastax.oss.driver.api.core._ +import com.datastax.oss.driver.internal.core.metadata.TopologyEvent +import com.datastax.oss.driver.internal.core.context.InternalDriverContext +import java.net.InetSocketAddress + +import CqlSession + +// Heartbeat logs every 30 seconds are annoying in the console, raise the interval +System.setProperty("datastax-java-driver.advanced.heartbeat.interval", "1 hour") + +val address1 = new InetSocketAddress("127.0.0.1", 9042) +val address2 = new InetSocketAddress("127.0.0.2", 9042) +val address3 = new InetSocketAddress("127.0.0.3", 9042) +val address4 = new InetSocketAddress("127.0.0.4", 9042) +val address5 = new InetSocketAddress("127.0.0.5", 9042) +val address6 = new InetSocketAddress("127.0.0.6", 9042) + +val builder = CqlSession.builder().addContactPoint(address1) + +println("********************************************") +println("* To start a driver instance, run: *") +println("* implicit val session = builder.build *") +println("********************************************") + +def fire(event: AnyRef)(implicit session: CqlSession): Unit = { + session.getContext.asInstanceOf[InternalDriverContext].getEventBus().fire(event) +} \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 00000000000..e46538ebb6d --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,203 @@ + + + 4.0.0 + + + com.datastax.oss + java-driver-parent + 4.0.0 + + + java-driver-core + bundle + + DataStax Java driver for Apache Cassandra(R) - core + + + + com.datastax.oss + native-protocol + + + io.netty + netty-handler + + + com.datastax.oss + java-driver-shaded-guava + + + com.typesafe + config + + + + com.github.jnr + jnr-ffi + + + com.github.jnr + jnr-posix + + + org.xerial.snappy + snappy-java + true + + + org.lz4 + lz4-java + true + + + org.slf4j + slf4j-api + + + io.dropwizard.metrics + metrics-core + + + org.hdrhistogram + HdrHistogram + + + com.github.stephenc.jcip + jcip-annotations + + + com.github.spotbugs + spotbugs-annotations + + + ch.qos.logback + logback-classic + test + + + junit + junit + test + + + com.tngtech.java + junit-dataprovider + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + + + + src/main/resources + + com/datastax/oss/driver/Driver.properties + + true + + + src/main/resources + + com/datastax/oss/driver/Driver.properties + + false + + + + + maven-jar-plugin + + + test-jar + + test-jar + + + + logback-test.xml + + + + + + + maven-surefire-plugin + + + + listener + com.datastax.oss.driver.DriverRunListener + + + + + + org.apache.felix + maven-bundle-plugin + true + + + + bundle + + + + com.datastax.oss.driver.core + + * + + + !net.jcip.annotations.*, + !edu.umd.cs.findbugs.annotations.*, + !jnr.*, + * + + + com.datastax.oss.driver.*.core.* + + + + + + + + + diff --git a/core/revapi.json b/core/revapi.json new file mode 100644 index 00000000000..b4b80558561 --- /dev/null +++ b/core/revapi.json @@ -0,0 +1,4727 @@ +// Configures Revapi (https://revapi.org/getting-started.html) to check API compatibility between +// successive driver versions. +{ + "revapi": { + "java": { + "filter": { + "packages": { + "regex": true, + "exclude": [ + "com\\.datastax\\.oss\\.protocol\\.internal(\\..+)?", + "com\\.datastax\\.oss\\.driver\\.internal(\\..+)?", + "com\\.datastax\\.oss\\.driver\\.shaded(\\..+)?", + "org\\.assertj(\\..+)?" + ] + } + } + }, + "ignore": [ + { + "code": "java.method.removed", + "old": "method com.datastax.oss.driver.api.core.cql.BatchStatementBuilder com.datastax.oss.driver.api.core.cql.BatchStatementBuilder::withKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method com.datastax.oss.driver.api.core.cql.BatchStatementBuilder com.datastax.oss.driver.api.core.cql.BatchStatementBuilder::withKeyspace(java.lang.String)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder::withKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder::withKeyspace(java.lang.String)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder::withQuery(java.lang.String)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withExecutionProfileName(java.lang.String)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withIdempotence(java.lang.Boolean)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withNode(com.datastax.oss.driver.api.core.metadata.Node)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withPageSize(int)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withPagingState(java.nio.ByteBuffer)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withRoutingKey(java.nio.ByteBuffer)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withRoutingKeyspace(java.lang.String)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withTimeout(java.time.Duration)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withTimestamp(long)", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.StatementBuilder>, StatementT>, StatementT extends com.datastax.oss.driver.api.core.cql.Statement>>::withTracing()", + "justification": "JAVA-2164: Rename statement builder methods to setXxx" + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter void com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException::(===java.net.SocketAddress===, java.lang.String, java.util.List)", + "new": "parameter void com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException::(===com.datastax.oss.driver.api.core.metadata.EndPoint===, java.lang.String, java.util.List)", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException::forNegotiation(===java.net.SocketAddress===, java.util.List)", + "new": "parameter com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException::forNegotiation(===com.datastax.oss.driver.api.core.metadata.EndPoint===, java.util.List)", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException::forSingleAttempt(===java.net.SocketAddress===, com.datastax.oss.driver.api.core.ProtocolVersion)", + "new": "parameter com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException::forSingleAttempt(===com.datastax.oss.driver.api.core.metadata.EndPoint===, com.datastax.oss.driver.api.core.ProtocolVersion)", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.removed", + "old": "method java.net.SocketAddress com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException::getAddress()", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter com.datastax.oss.driver.api.core.auth.Authenticator com.datastax.oss.driver.api.core.auth.AuthProvider::newAuthenticator(===java.net.SocketAddress===, java.lang.String) throws com.datastax.oss.driver.api.core.auth.AuthenticationException", + "new": "parameter com.datastax.oss.driver.api.core.auth.Authenticator com.datastax.oss.driver.api.core.auth.AuthProvider::newAuthenticator(===com.datastax.oss.driver.api.core.metadata.EndPoint===, java.lang.String) throws com.datastax.oss.driver.api.core.auth.AuthenticationException", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter void com.datastax.oss.driver.api.core.auth.AuthProvider::onMissingChallenge(===java.net.SocketAddress===) throws com.datastax.oss.driver.api.core.auth.AuthenticationException", + "new": "parameter void com.datastax.oss.driver.api.core.auth.AuthProvider::onMissingChallenge(===com.datastax.oss.driver.api.core.metadata.EndPoint===) throws com.datastax.oss.driver.api.core.auth.AuthenticationException", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter void com.datastax.oss.driver.api.core.auth.AuthenticationException::(===java.net.SocketAddress===, java.lang.String)", + "new": "parameter void com.datastax.oss.driver.api.core.auth.AuthenticationException::(===com.datastax.oss.driver.api.core.metadata.EndPoint===, java.lang.String)", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter void com.datastax.oss.driver.api.core.auth.AuthenticationException::(===java.net.SocketAddress===, java.lang.String, java.lang.Throwable)", + "new": "parameter void com.datastax.oss.driver.api.core.auth.AuthenticationException::(===com.datastax.oss.driver.api.core.metadata.EndPoint===, java.lang.String, java.lang.Throwable)", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.removed", + "old": "method java.net.SocketAddress com.datastax.oss.driver.api.core.auth.AuthenticationException::getAddress()", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.numberOfParametersChanged", + "old": "method void com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy::init(java.util.Map, com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy.DistanceReporter, java.util.Set)", + "new": "method void com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy::init(java.util.Map, com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy.DistanceReporter)", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.Metadata::getNodes()", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.Metadata::getNodes()", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.addedToInterface", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.Node::getBroadcastRpcAddress()", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.removed", + "old": "method java.net.InetSocketAddress com.datastax.oss.driver.api.core.metadata.Node::getConnectAddress()", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.addedToInterface", + "new": "method com.datastax.oss.driver.api.core.metadata.EndPoint com.datastax.oss.driver.api.core.metadata.Node::getEndPoint()", + "package": "com.datastax.oss.driver.api.core.metadata", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter javax.net.ssl.SSLEngine com.datastax.oss.driver.api.core.ssl.SslEngineFactory::newSslEngine(===java.net.SocketAddress===)", + "new": "parameter javax.net.ssl.SSLEngine com.datastax.oss.driver.api.core.ssl.SslEngineFactory::newSslEngine(===com.datastax.oss.driver.api.core.metadata.EndPoint===)", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.addedToInterface", + "new": "method long com.datastax.oss.driver.api.core.cql.Statement>>::getQueryTimestamp()", + "justification": "JAVA-2143: Rename Statement.setTimestamp() to setQueryTimestamp()" + }, + { + "code": "java.method.removed", + "old": "method long com.datastax.oss.driver.api.core.cql.Statement>>::getTimestamp()", + "justification": "JAVA-2143: Rename Statement.setTimestamp() to setQueryTimestamp()" + }, + { + "code": "java.method.addedToInterface", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setQueryTimestamp(long)", + "justification": "JAVA-2143: Rename Statement.setTimestamp() to setQueryTimestamp()" + }, + { + "code": "java.method.removed", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimestamp(long)", + "justification": "JAVA-2143: Rename Statement.setTimestamp() to setQueryTimestamp()" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::copy(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::copy(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setCustomPayload(java.util.Map) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setCustomPayload(java.util.Map) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfileName(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfileName(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setIdempotent(java.lang.Boolean) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setIdempotent(java.lang.Boolean) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setNode(com.datastax.oss.driver.api.core.metadata.Node) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setNode(com.datastax.oss.driver.api.core.metadata.Node) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPageSize(int) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPageSize(int) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPagingState(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPagingState(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer[]) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer[]) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimeout(java.time.Duration) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimeout(java.time.Duration) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTracing(boolean) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTracing(boolean) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::copy(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::copy(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setCustomPayload(java.util.Map) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setCustomPayload(java.util.Map) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfileName(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfileName(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setIdempotent(java.lang.Boolean) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setIdempotent(java.lang.Boolean) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setNode(com.datastax.oss.driver.api.core.metadata.Node) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setNode(com.datastax.oss.driver.api.core.metadata.Node) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPageSize(int) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPageSize(int) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPagingState(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPagingState(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer[]) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer[]) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimeout(java.time.Duration) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimeout(java.time.Duration) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTracing(boolean) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTracing(boolean) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigDecimal(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigDecimal(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigDecimal(java.lang.String, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigDecimal(java.lang.String, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigInteger(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigInteger(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigInteger(java.lang.String, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigInteger(java.lang.String, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBoolean(com.datastax.oss.driver.api.core.CqlIdentifier, boolean) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBoolean(com.datastax.oss.driver.api.core.CqlIdentifier, boolean) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBoolean(java.lang.String, boolean) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBoolean(java.lang.String, boolean) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByte(com.datastax.oss.driver.api.core.CqlIdentifier, byte) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByte(com.datastax.oss.driver.api.core.CqlIdentifier, byte) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByte(java.lang.String, byte) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByte(java.lang.String, byte) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByteBuffer(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByteBuffer(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByteBuffer(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByteBuffer(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBytesUnsafe(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBytesUnsafe(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBytesUnsafe(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBytesUnsafe(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setCqlDuration(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setCqlDuration(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setCqlDuration(java.lang.String, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setCqlDuration(java.lang.String, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setDouble(com.datastax.oss.driver.api.core.CqlIdentifier, double) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setDouble(com.datastax.oss.driver.api.core.CqlIdentifier, double) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setDouble(java.lang.String, double) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setDouble(java.lang.String, double) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setFloat(com.datastax.oss.driver.api.core.CqlIdentifier, float) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setFloat(com.datastax.oss.driver.api.core.CqlIdentifier, float) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setFloat(java.lang.String, float) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setFloat(java.lang.String, float) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInetAddress(com.datastax.oss.driver.api.core.CqlIdentifier, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInetAddress(com.datastax.oss.driver.api.core.CqlIdentifier, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInetAddress(java.lang.String, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInetAddress(java.lang.String, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInstant(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInstant(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInstant(java.lang.String, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInstant(java.lang.String, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInt(com.datastax.oss.driver.api.core.CqlIdentifier, int) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInt(com.datastax.oss.driver.api.core.CqlIdentifier, int) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInt(java.lang.String, int) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInt(java.lang.String, int) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setList(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setList(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setList(java.lang.String, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setList(java.lang.String, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalDate(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalDate(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalDate(java.lang.String, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalDate(java.lang.String, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalTime(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalTime(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalTime(java.lang.String, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalTime(java.lang.String, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLong(com.datastax.oss.driver.api.core.CqlIdentifier, long) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLong(com.datastax.oss.driver.api.core.CqlIdentifier, long) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLong(java.lang.String, long) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLong(java.lang.String, long) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setMap(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setMap(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setMap(java.lang.String, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setMap(java.lang.String, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setSet(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setSet(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setSet(java.lang.String, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setSet(java.lang.String, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setShort(com.datastax.oss.driver.api.core.CqlIdentifier, short) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setShort(com.datastax.oss.driver.api.core.CqlIdentifier, short) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setShort(java.lang.String, short) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setShort(java.lang.String, short) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setString(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.String) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setString(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.String) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setString(java.lang.String, java.lang.String) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setString(java.lang.String, java.lang.String) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToNull(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToNull(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToNull(java.lang.String) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToNull(java.lang.String) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToken(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToken(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToken(java.lang.String, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToken(java.lang.String, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setTupleValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setTupleValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setTupleValue(java.lang.String, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setTupleValue(java.lang.String, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUdtValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUdtValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUdtValue(java.lang.String, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUdtValue(java.lang.String, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUuid(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUuid(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUuid(java.lang.String, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUuid(java.lang.String, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.Bindable>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(com.datastax.oss.driver.api.core.CqlIdentifier)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(com.datastax.oss.driver.api.core.CqlIdentifier)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(int)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(int)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(java.lang.String)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(java.lang.String)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::copy(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::copy(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigDecimal(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigDecimal(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigDecimal(java.lang.String, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigDecimal(java.lang.String, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigInteger(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigInteger(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigInteger(java.lang.String, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigInteger(java.lang.String, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBoolean(com.datastax.oss.driver.api.core.CqlIdentifier, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBoolean(com.datastax.oss.driver.api.core.CqlIdentifier, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBoolean(java.lang.String, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBoolean(java.lang.String, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByte(com.datastax.oss.driver.api.core.CqlIdentifier, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByte(com.datastax.oss.driver.api.core.CqlIdentifier, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByte(java.lang.String, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByte(java.lang.String, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByteBuffer(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByteBuffer(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByteBuffer(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByteBuffer(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBytesUnsafe(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBytesUnsafe(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBytesUnsafe(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBytesUnsafe(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setCqlDuration(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setCqlDuration(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setCqlDuration(java.lang.String, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setCqlDuration(java.lang.String, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setCustomPayload(java.util.Map) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setCustomPayload(java.util.Map) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setDouble(com.datastax.oss.driver.api.core.CqlIdentifier, double) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setDouble(com.datastax.oss.driver.api.core.CqlIdentifier, double) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setDouble(java.lang.String, double) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setDouble(java.lang.String, double) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfileName(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfileName(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setFloat(com.datastax.oss.driver.api.core.CqlIdentifier, float) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setFloat(com.datastax.oss.driver.api.core.CqlIdentifier, float) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setFloat(java.lang.String, float) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setFloat(java.lang.String, float) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setIdempotent(java.lang.Boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setIdempotent(java.lang.Boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInetAddress(com.datastax.oss.driver.api.core.CqlIdentifier, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInetAddress(com.datastax.oss.driver.api.core.CqlIdentifier, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInetAddress(java.lang.String, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInetAddress(java.lang.String, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInstant(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInstant(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInstant(java.lang.String, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInstant(java.lang.String, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInt(com.datastax.oss.driver.api.core.CqlIdentifier, int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInt(com.datastax.oss.driver.api.core.CqlIdentifier, int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInt(java.lang.String, int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInt(java.lang.String, int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setList(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setList(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setList(java.lang.String, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setList(java.lang.String, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalDate(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalDate(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalDate(java.lang.String, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalDate(java.lang.String, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalTime(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalTime(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalTime(java.lang.String, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalTime(java.lang.String, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLong(com.datastax.oss.driver.api.core.CqlIdentifier, long) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLong(com.datastax.oss.driver.api.core.CqlIdentifier, long) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLong(java.lang.String, long) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLong(java.lang.String, long) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setMap(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setMap(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setMap(java.lang.String, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setMap(java.lang.String, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setNode(com.datastax.oss.driver.api.core.metadata.Node) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setNode(com.datastax.oss.driver.api.core.metadata.Node) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPageSize(int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPageSize(int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPagingState(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPagingState(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer[]) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer[]) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setSet(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setSet(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setSet(java.lang.String, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setSet(java.lang.String, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setShort(com.datastax.oss.driver.api.core.CqlIdentifier, short) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setShort(com.datastax.oss.driver.api.core.CqlIdentifier, short) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setShort(java.lang.String, short) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setShort(java.lang.String, short) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setString(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setString(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setString(java.lang.String, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setString(java.lang.String, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimeout(java.time.Duration) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimeout(java.time.Duration) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToNull(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToNull(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToNull(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToNull(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToken(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToken(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToken(java.lang.String, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToken(java.lang.String, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTracing(boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTracing(boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setTupleValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setTupleValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setTupleValue(java.lang.String, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setTupleValue(java.lang.String, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUdtValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUdtValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUdtValue(java.lang.String, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUdtValue(java.lang.String, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUuid(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUuid(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUuid(java.lang.String, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUuid(java.lang.String, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(int) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigDecimal(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigDecimal(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigDecimal(java.lang.String, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigDecimal(java.lang.String, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigInteger(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigInteger(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigInteger(java.lang.String, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigInteger(java.lang.String, java.math.BigInteger) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBoolean(com.datastax.oss.driver.api.core.CqlIdentifier, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBoolean(com.datastax.oss.driver.api.core.CqlIdentifier, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBoolean(java.lang.String, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBoolean(java.lang.String, boolean) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByte(com.datastax.oss.driver.api.core.CqlIdentifier, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByte(com.datastax.oss.driver.api.core.CqlIdentifier, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByte(java.lang.String, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByte(java.lang.String, byte) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByteBuffer(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByteBuffer(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByteBuffer(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByteBuffer(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBytesUnsafe(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBytesUnsafe(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBytesUnsafe(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBytesUnsafe(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setCqlDuration(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setCqlDuration(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setCqlDuration(java.lang.String, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setCqlDuration(java.lang.String, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setDouble(com.datastax.oss.driver.api.core.CqlIdentifier, double) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setDouble(com.datastax.oss.driver.api.core.CqlIdentifier, double) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setDouble(java.lang.String, double) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setDouble(java.lang.String, double) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setFloat(com.datastax.oss.driver.api.core.CqlIdentifier, float) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setFloat(com.datastax.oss.driver.api.core.CqlIdentifier, float) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setFloat(java.lang.String, float) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setFloat(java.lang.String, float) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInetAddress(com.datastax.oss.driver.api.core.CqlIdentifier, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInetAddress(com.datastax.oss.driver.api.core.CqlIdentifier, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInetAddress(java.lang.String, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInetAddress(java.lang.String, java.net.InetAddress) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInstant(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInstant(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInstant(java.lang.String, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInstant(java.lang.String, java.time.Instant) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInt(com.datastax.oss.driver.api.core.CqlIdentifier, int) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInt(com.datastax.oss.driver.api.core.CqlIdentifier, int) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInt(java.lang.String, int) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInt(java.lang.String, int) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setList(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setList(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setList(java.lang.String, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setList(java.lang.String, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalDate(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalDate(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalDate(java.lang.String, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalDate(java.lang.String, java.time.LocalDate) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalTime(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalTime(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalTime(java.lang.String, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalTime(java.lang.String, java.time.LocalTime) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLong(com.datastax.oss.driver.api.core.CqlIdentifier, long) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLong(com.datastax.oss.driver.api.core.CqlIdentifier, long) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLong(java.lang.String, long) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLong(java.lang.String, long) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setMap(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setMap(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setMap(java.lang.String, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setMap(java.lang.String, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setSet(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setSet(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setSet(java.lang.String, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setSet(java.lang.String, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setShort(com.datastax.oss.driver.api.core.CqlIdentifier, short) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setShort(com.datastax.oss.driver.api.core.CqlIdentifier, short) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setShort(java.lang.String, short) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setShort(java.lang.String, short) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setString(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setString(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setString(java.lang.String, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setString(java.lang.String, java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToNull(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToNull(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToNull(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToNull(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToken(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToken(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToken(java.lang.String, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToken(java.lang.String, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setTupleValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setTupleValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setTupleValue(java.lang.String, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setTupleValue(java.lang.String, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUdtValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUdtValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUdtValue(java.lang.String, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUdtValue(java.lang.String, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUuid(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUuid(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUuid(java.lang.String, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUuid(java.lang.String, java.util.UUID) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(int) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(int) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Bindable>>::unset(java.lang.String) @ com.datastax.oss.driver.api.core.cql.BoundStatementBuilder", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::copy(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::copy(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setCustomPayload(java.util.Map) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setCustomPayload(java.util.Map) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfileName(java.lang.String) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfileName(java.lang.String) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setIdempotent(java.lang.Boolean) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setIdempotent(java.lang.Boolean) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setNode(com.datastax.oss.driver.api.core.metadata.Node) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setNode(com.datastax.oss.driver.api.core.metadata.Node) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPageSize(int) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPageSize(int) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPagingState(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPagingState(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer[]) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer[]) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(java.lang.String) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(java.lang.String) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimeout(java.time.Duration) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimeout(java.time.Duration) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTracing(boolean) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTracing(boolean) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::copy(java.nio.ByteBuffer)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::copy(java.nio.ByteBuffer)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setCustomPayload(java.util.Map)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setCustomPayload(java.util.Map)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfile(com.datastax.oss.driver.api.core.config.DriverExecutionProfile)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfileName(java.lang.String)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setExecutionProfileName(java.lang.String)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setIdempotent(java.lang.Boolean)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setIdempotent(java.lang.Boolean)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setNode(com.datastax.oss.driver.api.core.metadata.Node)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setNode(com.datastax.oss.driver.api.core.metadata.Node)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPageSize(int)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPageSize(int)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPagingState(java.nio.ByteBuffer)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setPagingState(java.nio.ByteBuffer)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer[])", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKey(java.nio.ByteBuffer[])", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(java.lang.String)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingKeyspace(java.lang.String)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setRoutingToken(com.datastax.oss.driver.api.core.metadata.token.Token)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setSerialConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimeout(java.time.Duration)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTimeout(java.time.Duration)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTracing(boolean)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setTracing(boolean)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigDecimal(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigDecimal)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigDecimal(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigDecimal)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigInteger(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigInteger)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigInteger(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigInteger)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBoolean(com.datastax.oss.driver.api.core.CqlIdentifier, boolean)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBoolean(com.datastax.oss.driver.api.core.CqlIdentifier, boolean)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByte(com.datastax.oss.driver.api.core.CqlIdentifier, byte)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByte(com.datastax.oss.driver.api.core.CqlIdentifier, byte)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByteBuffer(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByteBuffer(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBytesUnsafe(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBytesUnsafe(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setCqlDuration(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.CqlDuration)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setCqlDuration(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.CqlDuration)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setDouble(com.datastax.oss.driver.api.core.CqlIdentifier, double)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setDouble(com.datastax.oss.driver.api.core.CqlIdentifier, double)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setFloat(com.datastax.oss.driver.api.core.CqlIdentifier, float)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setFloat(com.datastax.oss.driver.api.core.CqlIdentifier, float)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInetAddress(com.datastax.oss.driver.api.core.CqlIdentifier, java.net.InetAddress)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInetAddress(com.datastax.oss.driver.api.core.CqlIdentifier, java.net.InetAddress)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInstant(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.Instant)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInstant(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.Instant)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInt(com.datastax.oss.driver.api.core.CqlIdentifier, int)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInt(com.datastax.oss.driver.api.core.CqlIdentifier, int)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setList(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.List, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setList(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.List, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalDate(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalDate)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalDate(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalDate)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalTime(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalTime)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalTime(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalTime)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLong(com.datastax.oss.driver.api.core.CqlIdentifier, long)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLong(com.datastax.oss.driver.api.core.CqlIdentifier, long)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setMap(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Map, java.lang.Class, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setMap(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Map, java.lang.Class, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setSet(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Set, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setSet(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Set, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setShort(com.datastax.oss.driver.api.core.CqlIdentifier, short)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setShort(com.datastax.oss.driver.api.core.CqlIdentifier, short)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setString(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.String)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setString(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.String)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToNull(com.datastax.oss.driver.api.core.CqlIdentifier)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToNull(com.datastax.oss.driver.api.core.CqlIdentifier)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToken(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.metadata.token.Token)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToken(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.metadata.token.Token)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setTupleValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.TupleValue)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setTupleValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.TupleValue)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUdtValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.UdtValue)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUdtValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.UdtValue)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUuid(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.UUID)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUuid(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.UUID)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.data.SettableById>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigDecimal(java.lang.String, java.math.BigDecimal)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigDecimal(java.lang.String, java.math.BigDecimal)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigInteger(java.lang.String, java.math.BigInteger)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigInteger(java.lang.String, java.math.BigInteger)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBoolean(java.lang.String, boolean)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBoolean(java.lang.String, boolean)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByte(java.lang.String, byte)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByte(java.lang.String, byte)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByteBuffer(java.lang.String, java.nio.ByteBuffer)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByteBuffer(java.lang.String, java.nio.ByteBuffer)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBytesUnsafe(java.lang.String, java.nio.ByteBuffer)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBytesUnsafe(java.lang.String, java.nio.ByteBuffer)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setCqlDuration(java.lang.String, com.datastax.oss.driver.api.core.data.CqlDuration)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setCqlDuration(java.lang.String, com.datastax.oss.driver.api.core.data.CqlDuration)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setDouble(java.lang.String, double)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setDouble(java.lang.String, double)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setFloat(java.lang.String, float)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setFloat(java.lang.String, float)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInetAddress(java.lang.String, java.net.InetAddress)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInetAddress(java.lang.String, java.net.InetAddress)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInstant(java.lang.String, java.time.Instant)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInstant(java.lang.String, java.time.Instant)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInt(java.lang.String, int)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInt(java.lang.String, int)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setList(java.lang.String, java.util.List, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setList(java.lang.String, java.util.List, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalDate(java.lang.String, java.time.LocalDate)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalDate(java.lang.String, java.time.LocalDate)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalTime(java.lang.String, java.time.LocalTime)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalTime(java.lang.String, java.time.LocalTime)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLong(java.lang.String, long)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLong(java.lang.String, long)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setMap(java.lang.String, java.util.Map, java.lang.Class, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setMap(java.lang.String, java.util.Map, java.lang.Class, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setSet(java.lang.String, java.util.Set, java.lang.Class)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setSet(java.lang.String, java.util.Set, java.lang.Class)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setShort(java.lang.String, short)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setShort(java.lang.String, short)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setString(java.lang.String, java.lang.String)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setString(java.lang.String, java.lang.String)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToNull(java.lang.String)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToNull(java.lang.String)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToken(java.lang.String, com.datastax.oss.driver.api.core.metadata.token.Token)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToken(java.lang.String, com.datastax.oss.driver.api.core.metadata.token.Token)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setTupleValue(java.lang.String, com.datastax.oss.driver.api.core.data.TupleValue)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setTupleValue(java.lang.String, com.datastax.oss.driver.api.core.data.TupleValue)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUdtValue(java.lang.String, com.datastax.oss.driver.api.core.data.UdtValue)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUdtValue(java.lang.String, com.datastax.oss.driver.api.core.data.UdtValue)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.data.SettableByName>>", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUuid(java.lang.String, java.util.UUID)", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUuid(java.lang.String, java.util.UUID)", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.data.TupleValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.data.TupleValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::set(com.datastax.oss.driver.api.core.CqlIdentifier, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::set(int, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.codec.TypeCodec) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, com.datastax.oss.driver.api.core.type.reflect.GenericType) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::set(java.lang.String, ValueT, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigDecimal(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigDecimal(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigDecimal(int, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigDecimal(java.lang.String, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigDecimal(java.lang.String, java.math.BigDecimal) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigInteger(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBigInteger(com.datastax.oss.driver.api.core.CqlIdentifier, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBigInteger(int, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigInteger(java.lang.String, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBigInteger(java.lang.String, java.math.BigInteger) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBoolean(com.datastax.oss.driver.api.core.CqlIdentifier, boolean) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBoolean(com.datastax.oss.driver.api.core.CqlIdentifier, boolean) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBoolean(int, boolean) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBoolean(java.lang.String, boolean) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBoolean(java.lang.String, boolean) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByte(com.datastax.oss.driver.api.core.CqlIdentifier, byte) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByte(com.datastax.oss.driver.api.core.CqlIdentifier, byte) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByte(int, byte) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByte(java.lang.String, byte) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByte(java.lang.String, byte) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByteBuffer(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setByteBuffer(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setByteBuffer(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByteBuffer(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setByteBuffer(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBytesUnsafe(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setBytesUnsafe(com.datastax.oss.driver.api.core.CqlIdentifier, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setBytesUnsafe(int, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBytesUnsafe(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setBytesUnsafe(java.lang.String, java.nio.ByteBuffer) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setCqlDuration(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setCqlDuration(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setCqlDuration(int, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setCqlDuration(java.lang.String, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setCqlDuration(java.lang.String, com.datastax.oss.driver.api.core.data.CqlDuration) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setDouble(com.datastax.oss.driver.api.core.CqlIdentifier, double) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setDouble(com.datastax.oss.driver.api.core.CqlIdentifier, double) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setDouble(int, double) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setDouble(java.lang.String, double) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setDouble(java.lang.String, double) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setFloat(com.datastax.oss.driver.api.core.CqlIdentifier, float) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setFloat(com.datastax.oss.driver.api.core.CqlIdentifier, float) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setFloat(int, float) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setFloat(java.lang.String, float) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setFloat(java.lang.String, float) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInetAddress(com.datastax.oss.driver.api.core.CqlIdentifier, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInetAddress(com.datastax.oss.driver.api.core.CqlIdentifier, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInetAddress(int, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInetAddress(java.lang.String, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInetAddress(java.lang.String, java.net.InetAddress) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInstant(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.Instant) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInstant(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.Instant) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInstant(int, java.time.Instant) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInstant(java.lang.String, java.time.Instant) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInstant(java.lang.String, java.time.Instant) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInt(com.datastax.oss.driver.api.core.CqlIdentifier, int) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setInt(com.datastax.oss.driver.api.core.CqlIdentifier, int) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setInt(int, int) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInt(java.lang.String, int) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setInt(java.lang.String, int) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setList(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setList(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setList(int, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setList(java.lang.String, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setList(java.lang.String, java.util.List, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalDate(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalDate(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalDate(int, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalDate(java.lang.String, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalDate(java.lang.String, java.time.LocalDate) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalTime(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLocalTime(com.datastax.oss.driver.api.core.CqlIdentifier, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLocalTime(int, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalTime(java.lang.String, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLocalTime(java.lang.String, java.time.LocalTime) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLong(com.datastax.oss.driver.api.core.CqlIdentifier, long) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setLong(com.datastax.oss.driver.api.core.CqlIdentifier, long) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setLong(int, long) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLong(java.lang.String, long) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setLong(java.lang.String, long) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setMap(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setMap(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setMap(int, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setMap(java.lang.String, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setMap(java.lang.String, java.util.Map, java.lang.Class, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setSet(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setSet(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setSet(int, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setSet(java.lang.String, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setSet(java.lang.String, java.util.Set, java.lang.Class) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setShort(com.datastax.oss.driver.api.core.CqlIdentifier, short) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setShort(com.datastax.oss.driver.api.core.CqlIdentifier, short) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setShort(int, short) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setShort(java.lang.String, short) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setShort(java.lang.String, short) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setString(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.String) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setString(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.String) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setString(int, java.lang.String) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setString(java.lang.String, java.lang.String) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setString(java.lang.String, java.lang.String) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToNull(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToNull(com.datastax.oss.driver.api.core.CqlIdentifier) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToNull(int) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToNull(java.lang.String) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToNull(java.lang.String) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToken(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setToken(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setToken(int, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToken(java.lang.String, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setToken(java.lang.String, com.datastax.oss.driver.api.core.metadata.token.Token) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setTupleValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setTupleValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setTupleValue(int, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setTupleValue(java.lang.String, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setTupleValue(java.lang.String, com.datastax.oss.driver.api.core.data.TupleValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUdtValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUdtValue(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUdtValue(int, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUdtValue(java.lang.String, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUdtValue(java.lang.String, com.datastax.oss.driver.api.core.data.UdtValue) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUuid(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.UUID) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableById>>::setUuid(com.datastax.oss.driver.api.core.CqlIdentifier, java.util.UUID) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByIndex>>::setUuid(int, java.util.UUID) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUuid(java.lang.String, java.util.UUID) @ com.datastax.oss.driver.api.core.data.UdtValue", + "new": "method SelfT com.datastax.oss.driver.api.core.data.SettableByName>>::setUuid(java.lang.String, java.util.UUID) @ com.datastax.oss.driver.api.core.data.UdtValue", + "annotation": "@edu.umd.cs.findbugs.annotations.CheckReturnValue", + "justification": "JAVA-2161: Annotate mutating methods with @CheckReturnValue" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.NonNull", + "justification": "Add missing `@NonNull` annotation to Statement.setConsistencyLevel" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BatchableStatement>>", + "annotation": "@edu.umd.cs.findbugs.annotations.NonNull", + "justification": "Add missing `@NonNull` annotation to Statement.setConsistencyLevel" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.BoundStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.NonNull", + "justification": "Add missing `@NonNull` annotation to Statement.setConsistencyLevel" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel) @ com.datastax.oss.driver.api.core.cql.SimpleStatement", + "annotation": "@edu.umd.cs.findbugs.annotations.NonNull", + "justification": "Add missing `@NonNull` annotation to Statement.setConsistencyLevel" + }, + { + "code": "java.annotation.added", + "old": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel)", + "new": "method SelfT com.datastax.oss.driver.api.core.cql.Statement>>::setConsistencyLevel(com.datastax.oss.driver.api.core.ConsistencyLevel)", + "annotation": "@edu.umd.cs.findbugs.annotations.NonNull", + "justification": "Add missing `@NonNull` annotation to Statement.setConsistencyLevel" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.concurrent.CompletionStage> com.datastax.oss.driver.api.core.AsyncPagingIterable::fetchNextPage() throws java.lang.IllegalStateException", + "new": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.AsyncPagingIterable>>::fetchNextPage() throws java.lang.IllegalStateException", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeChangedCovariantly", + "old": "method com.datastax.oss.driver.api.core.AsyncPagingIterable com.datastax.oss.driver.api.core.AsyncPagingIterable::map(java.util.function.Function)", + "new": "method com.datastax.oss.driver.api.core.MappedAsyncPagingIterable com.datastax.oss.driver.api.core.AsyncPagingIterable>>::map(java.util.function.Function)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.generics.formalTypeParameterAdded", + "old": "interface com.datastax.oss.driver.api.core.AsyncPagingIterable", + "new": "interface com.datastax.oss.driver.api.core.AsyncPagingIterable>", + "typeParameter": "SelfT extends com.datastax.oss.driver.api.core.AsyncPagingIterable>", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.CqlSession::executeAsync(com.datastax.oss.driver.api.core.cql.Statement)", + "new": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.CqlSession::executeAsync(com.datastax.oss.driver.api.core.cql.Statement)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.CqlSession::executeAsync(java.lang.String)", + "new": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.CqlSession::executeAsync(java.lang.String)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.CqlSession::prepareAsync(com.datastax.oss.driver.api.core.cql.PrepareRequest)", + "new": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.CqlSession::prepareAsync(com.datastax.oss.driver.api.core.cql.PrepareRequest)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.CqlSession::prepareAsync(com.datastax.oss.driver.api.core.cql.SimpleStatement)", + "new": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.CqlSession::prepareAsync(com.datastax.oss.driver.api.core.cql.SimpleStatement)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.CqlSession::prepareAsync(java.lang.String)", + "new": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.CqlSession::prepareAsync(java.lang.String)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.cql.AsyncResultSet::fetchNextPage() throws java.lang.IllegalStateException", + "new": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.AsyncPagingIterable>>::fetchNextPage() throws java.lang.IllegalStateException @ com.datastax.oss.driver.api.core.cql.AsyncResultSet", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.class.noLongerImplementsInterface", + "old": "interface com.datastax.oss.driver.api.core.cql.AsyncResultSet", + "new": "interface com.datastax.oss.driver.api.core.cql.AsyncResultSet", + "interface": "com.datastax.oss.driver.api.core.AsyncPagingIterable", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.class.superTypeTypeParametersChanged", + "old": "interface com.datastax.oss.driver.api.core.cql.AsyncResultSet", + "new": "interface com.datastax.oss.driver.api.core.cql.AsyncResultSet", + "oldSuperType": "com.datastax.oss.driver.api.core.AsyncPagingIterable", + "newSuperType": "com.datastax.oss.driver.api.core.AsyncPagingIterable", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.session.Session::refreshSchemaAsync()", + "new": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.session.Session::refreshSchemaAsync()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.session.Session::setSchemaMetadataEnabled(java.lang.Boolean)", + "new": "method java.util.concurrent.CompletionStage com.datastax.oss.driver.api.core.session.Session::setSchemaMetadataEnabled(java.lang.Boolean)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.session.Session::getMetrics()", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.session.Session::getMetrics()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.Metadata::getKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.Metadata::getKeyspace(com.datastax.oss.driver.api.core.CqlIdentifier)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.Metadata::getKeyspace(java.lang.String)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.Metadata::getKeyspace(java.lang.String)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.Metadata::getKeyspaces()", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.Metadata::getKeyspaces()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.Metadata::getTokenMap()", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.Metadata::getTokenMap()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregate(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.type.DataType[])", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregate(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.type.DataType[])", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregate(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.Iterable)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregate(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.Iterable)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregate(com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregate(com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregate(java.lang.String, com.datastax.oss.driver.api.core.type.DataType[])", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregate(java.lang.String, com.datastax.oss.driver.api.core.type.DataType[])", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregate(java.lang.String, java.lang.Iterable)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregate(java.lang.String, java.lang.Iterable)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregates()", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getAggregates()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunction(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.type.DataType[])", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunction(com.datastax.oss.driver.api.core.CqlIdentifier, com.datastax.oss.driver.api.core.type.DataType[])", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunction(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.Iterable)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunction(com.datastax.oss.driver.api.core.CqlIdentifier, java.lang.Iterable)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunction(com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunction(com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunction(java.lang.String, com.datastax.oss.driver.api.core.type.DataType[])", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunction(java.lang.String, com.datastax.oss.driver.api.core.type.DataType[])", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunction(java.lang.String, java.lang.Iterable)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunction(java.lang.String, java.lang.Iterable)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunctions()", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getFunctions()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getTable(com.datastax.oss.driver.api.core.CqlIdentifier)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getTable(com.datastax.oss.driver.api.core.CqlIdentifier)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getTable(java.lang.String)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getTable(java.lang.String)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getTables()", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getTables()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getUserDefinedType(com.datastax.oss.driver.api.core.CqlIdentifier)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getUserDefinedType(com.datastax.oss.driver.api.core.CqlIdentifier)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getUserDefinedType(java.lang.String)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getUserDefinedType(java.lang.String)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getUserDefinedTypes()", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getUserDefinedTypes()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getView(com.datastax.oss.driver.api.core.CqlIdentifier)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getView(com.datastax.oss.driver.api.core.CqlIdentifier)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getView(java.lang.String)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getView(java.lang.String)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getViews()", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getViews()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getViewsOnTable(com.datastax.oss.driver.api.core.CqlIdentifier)", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata::getViewsOnTable(com.datastax.oss.driver.api.core.CqlIdentifier)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getClusteringColumns()", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getClusteringColumns()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getColumn(com.datastax.oss.driver.api.core.CqlIdentifier)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getColumn(com.datastax.oss.driver.api.core.CqlIdentifier)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getColumn(java.lang.String)", + "new": "method java.util.Optional com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getColumn(java.lang.String)", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getColumns()", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getColumns()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.List com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getPartitionKey()", + "new": "method java.util.List com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getPartitionKey()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.List com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getPrimaryKey()", + "new": "method java.util.List com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata::getPrimaryKey()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.TableMetadata::getIndexes()", + "new": "method java.util.Map com.datastax.oss.driver.api.core.metadata.schema.TableMetadata::getIndexes()", + "justification": "JAVA-2192: Don't return generic types with wildcards" + } + ] + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/AllNodesFailedException.java b/core/src/main/java/com/datastax/oss/driver/api/core/AllNodesFailedException.java new file mode 100644 index 00000000000..a897c4d9e27 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/AllNodesFailedException.java @@ -0,0 +1,103 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.shaded.guava.common.base.Joiner; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterables; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.List; +import java.util.Map; + +/** + * Thrown when a query failed on all the coordinators it was tried on. This exception may wrap + * multiple errors, use {@link #getErrors()} to inspect the individual problem on each node. + */ +public class AllNodesFailedException extends DriverException { + + @NonNull + public static AllNodesFailedException fromErrors(@Nullable Map errors) { + if (errors == null || errors.isEmpty()) { + return new NoNodeAvailableException(); + } else { + return new AllNodesFailedException(ImmutableMap.copyOf(errors)); + } + } + + @NonNull + public static AllNodesFailedException fromErrors( + @Nullable List> errors) { + Map map; + if (errors == null || errors.isEmpty()) { + map = null; + } else { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : errors) { + builder.put(entry); + } + map = builder.build(); + } + return fromErrors(map); + } + + private final Map errors; + + protected AllNodesFailedException( + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + @NonNull Map errors) { + super(message, executionInfo, null, true); + this.errors = errors; + } + + private AllNodesFailedException(Map errors) { + this( + buildMessage( + String.format("All %d node(s) tried for the query failed", errors.size()), errors), + null, + errors); + } + + private static String buildMessage(String baseMessage, Map errors) { + int limit = Math.min(errors.size(), 3); + String details = + Joiner.on(", ").withKeyValueSeparator(": ").join(Iterables.limit(errors.entrySet(), limit)); + + return String.format( + baseMessage + " (showing first %d, use getErrors() for more: %s)", limit, details); + } + + /** The details of the individual error on each node. */ + @NonNull + public Map getErrors() { + return errors; + } + + @NonNull + @Override + public DriverException copy() { + return new AllNodesFailedException(getMessage(), getExecutionInfo(), errors); + } + + @NonNull + public AllNodesFailedException reword(String newMessage) { + return new AllNodesFailedException( + buildMessage(newMessage, errors), getExecutionInfo(), errors); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/AsyncAutoCloseable.java b/core/src/main/java/com/datastax/oss/driver/api/core/AsyncAutoCloseable.java new file mode 100644 index 00000000000..f84cdf26c86 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/AsyncAutoCloseable.java @@ -0,0 +1,78 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.CompletionStage; + +/** + * An object that can be closed in an asynchronous, non-blocking manner. + * + *

For convenience, this extends the JDK's {@code AutoCloseable} in order to be usable in + * try-with-resource blocks (in that case, the blocking {@link #close()} will be used). + */ +public interface AsyncAutoCloseable extends AutoCloseable { + + /** + * Returns a stage that will complete when {@link #close()} or {@link #forceCloseAsync()} is + * called, and the shutdown sequence completes. + */ + @NonNull + CompletionStage closeFuture(); + + /** + * Whether shutdown has completed. + * + *

This is a shortcut for {@code closeFuture().toCompletableFuture().isDone()}. + */ + default boolean isClosed() { + return closeFuture().toCompletableFuture().isDone(); + } + + /** + * Initiates an orderly shutdown: no new requests are accepted, but all pending requests are + * allowed to complete normally. + * + * @return a stage that will complete when the shutdown sequence is complete. Multiple calls to + * this method or {@link #forceCloseAsync()} always return the same instance. + */ + @NonNull + CompletionStage closeAsync(); + + /** + * Initiates a forced shutdown of this instance: no new requests are accepted, and all pending + * requests will complete with an exception. + * + * @return a stage that will complete when the shutdown sequence is complete. Multiple calls to + * this method or {@link #close()} always return the same instance. + */ + @NonNull + CompletionStage forceCloseAsync(); + + /** + * {@inheritDoc} + * + *

This method is implemented by calling {@link #closeAsync()} and blocking on the result. This + * should not be called on a driver thread. + */ + @Override + default void close() { + BlockingOperation.checkNotDriverThread(); + CompletableFutures.getUninterruptibly(closeAsync().toCompletableFuture()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/AsyncPagingIterable.java b/core/src/main/java/com/datastax/oss/driver/api/core/AsyncPagingIterable.java new file mode 100644 index 00000000000..9abe4136f66 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/AsyncPagingIterable.java @@ -0,0 +1,107 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.internal.core.AsyncPagingIterableWrapper; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Iterator; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +/** + * An iterable of elements which are fetched asynchronously by the driver, possibly in multiple + * requests. + */ +public interface AsyncPagingIterable> { + + /** Metadata about the columns returned by the CQL request that was used to build this result. */ + @NonNull + ColumnDefinitions getColumnDefinitions(); + + /** Returns {@linkplain ExecutionInfo information about the execution} of this page of results. */ + @NonNull + ExecutionInfo getExecutionInfo(); + + /** How many rows are left before the current page is exhausted. */ + int remaining(); + + /** + * The elements in the current page. To keep iterating beyond that, use {@link #hasMorePages()} + * and {@link #fetchNextPage()}. + * + *

Note that this method always returns the same object, and that that object can only be + * iterated once: elements are "consumed" as they are read. + */ + @NonNull + Iterable currentPage(); + + /** + * Returns the next element, or {@code null} if the results are exhausted. + * + *

This is convenient for queries that are known to return exactly one element, for example + * count queries. + */ + @Nullable + default ElementT one() { + Iterator iterator = currentPage().iterator(); + return iterator.hasNext() ? iterator.next() : null; + } + + /** + * Whether there are more pages of results. If so, call {@link #fetchNextPage()} to fetch the next + * one asynchronously. + */ + boolean hasMorePages(); + + /** + * Fetch the next page of results asynchronously. + * + * @throws IllegalStateException if there are no more pages. Use {@link #hasMorePages()} to check + * if you can call this method. + */ + @NonNull + CompletionStage fetchNextPage() throws IllegalStateException; + + /** + * If the query that produced this result was a CQL conditional update, indicate whether it was + * successfully applied. + * + *

For consistency, this method always returns {@code true} for non-conditional queries + * (although there is no reason to call the method in that case). This is also the case for + * conditional DDL statements ({@code CREATE KEYSPACE... IF NOT EXISTS}, {@code CREATE TABLE... IF + * NOT EXISTS}), for which Cassandra doesn't return an {@code [applied]} column. + * + *

Note that, for versions of Cassandra strictly lower than 2.1.0-rc2, a server-side bug (CASSANDRA-7337) causes this + * method to always return {@code true} for batches containing conditional queries. + */ + boolean wasApplied(); + + /** + * Creates a new instance by transforming each element of this iterable with the provided + * function. + * + *

Note that both instances share the same underlying data: consuming elements from the + * transformed iterable will also consume them from this object, and vice-versa. + */ + default MappedAsyncPagingIterable map( + Function elementMapper) { + return new AsyncPagingIterableWrapper<>(this, elementMapper); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/ConsistencyLevel.java b/core/src/main/java/com/datastax/oss/driver/api/core/ConsistencyLevel.java new file mode 100644 index 00000000000..65e32308fca --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/ConsistencyLevel.java @@ -0,0 +1,48 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The consistency level of a request. + * + *

The only reason to model this as an interface (as opposed to an enum type) is to accommodate + * for custom protocol extensions. If you're connecting to a standard Apache Cassandra cluster, all + * {@code ConsistencyLevel}s are {@link DefaultConsistencyLevel} instances. + */ +public interface ConsistencyLevel { + + /** The numerical value that the level is encoded to in protocol frames. */ + int getProtocolCode(); + + /** The textual representation of the level in configuration files. */ + @NonNull + String name(); + + /** Whether this consistency level applies to the local datacenter only. */ + boolean isDcLocal(); + + /** + * Whether this consistency level is serial, that is, applies only to the "paxos" phase of a lightweight + * transaction. + * + *

Serial consistency levels are only meaningful when executing conditional updates ({@code + * INSERT}, {@code UPDATE} or {@code DELETE} statements with an {@code IF} condition). + */ + boolean isSerial(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/CqlIdentifier.java b/core/src/main/java/com/datastax/oss/driver/api/core/CqlIdentifier.java new file mode 100644 index 00000000000..89211d75382 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/CqlIdentifier.java @@ -0,0 +1,152 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.internal.core.util.Strings; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import net.jcip.annotations.Immutable; + +/** + * The identifier of CQL element (keyspace, table, column, etc). + * + *

It has two representations: + * + *

    + *
  • the "CQL" form, which is how you would type the identifier in a CQL query. It is + * case-insensitive unless enclosed in double quotation marks; in addition, identifiers that + * contain special characters (anything other than alphanumeric and underscore), or match CQL + * keywords, must be double-quoted (with inner double quotes escaped as {@code ""}). + *
  • the "internal" form, which is how the name is stored in Cassandra system tables. It is + * lower-case for case-sensitive identifiers, and in the exact case for case-sensitive + * identifiers. + *
+ * + * Examples: + * + * + * + * + * + * + * + * + * + *
Create statementCase-sensitive?CQL idInternal id
CREATE TABLE t(foo int PRIMARY KEY)Nofoofoo
CREATE TABLE t(Foo int PRIMARY KEY)Nofoofoo
CREATE TABLE t("Foo" int PRIMARY KEY)Yes"Foo"Foo
CREATE TABLE t("foo bar" int PRIMARY KEY)Yes"foo bar"foo bar
CREATE TABLE t("foo""bar" int PRIMARY KEY)Yes"foo""bar"foo"bar
CREATE TABLE t("create" int PRIMARY KEY)Yes (reserved keyword)"create"create
+ * + * This class provides a common representation and avoids any ambiguity about which form the + * identifier is in. Driver clients will generally want to create instances from the CQL form with + * {@link #fromCql(String)}. + * + *

There is no internal caching; if you reuse the same identifiers often, consider caching them + * in your application. + */ +@Immutable +public class CqlIdentifier implements Serializable { + + private static final long serialVersionUID = 1; + + // IMPLEMENTATION NOTES: + // This is used internally, and for all API methods where the overhead of requiring the client to + // create an instance is acceptable (metadata, statement.getKeyspace, etc.) + // One exception is named getters, where we keep raw strings with the 3.x rules. + + /** Creates an identifier from its {@link CqlIdentifier CQL form}. */ + @NonNull + public static CqlIdentifier fromCql(@NonNull String cql) { + Preconditions.checkNotNull(cql, "cql must not be null"); + final String internal; + if (Strings.isDoubleQuoted(cql)) { + internal = Strings.unDoubleQuote(cql); + } else { + internal = cql.toLowerCase(); + Preconditions.checkArgument( + !Strings.needsDoubleQuotes(internal), "Invalid CQL form [%s]: needs double quotes", cql); + } + return fromInternal(internal); + } + + /** Creates an identifier from its {@link CqlIdentifier internal form}. */ + @NonNull + public static CqlIdentifier fromInternal(@NonNull String internal) { + Preconditions.checkNotNull(internal, "internal must not be null"); + return new CqlIdentifier(internal); + } + + /** @serial */ + private final String internal; + + private CqlIdentifier(String internal) { + this.internal = internal; + } + + /** + * Returns the identifier in the "internal" format. + * + * @return the identifier in its exact case, unquoted. + */ + @NonNull + public String asInternal() { + return this.internal; + } + + /** + * Returns the identifier in a format appropriate for concatenation in a CQL query. + * + * @param pretty if {@code true}, use the shortest possible representation: if the identifier is + * case-insensitive, an unquoted, lower-case string, otherwise the double-quoted form. If + * {@code false}, always use the double-quoted form (this is slightly more efficient since we + * don't need to inspect the string). + */ + @NonNull + public String asCql(boolean pretty) { + if (pretty) { + return Strings.needsDoubleQuotes(internal) ? Strings.doubleQuote(internal) : internal; + } else { + return Strings.doubleQuote(internal); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof CqlIdentifier) { + CqlIdentifier that = (CqlIdentifier) other; + return this.internal.equals(that.internal); + } else { + return false; + } + } + + @Override + public int hashCode() { + return internal.hashCode(); + } + + @Override + public String toString() { + return internal; + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + Preconditions.checkNotNull(internal, "internal must not be null"); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/CqlSession.java b/core/src/main/java/com/datastax/oss/driver/api/core/CqlSession.java new file mode 100644 index 00000000000..04a98054dc0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/CqlSession.java @@ -0,0 +1,232 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.PrepareRequest; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.internal.core.cql.DefaultPrepareRequest; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import java.util.concurrent.CompletionStage; + +/** A specialized session with convenience methods to execute CQL statements. */ +public interface CqlSession extends Session { + + /** Returns a builder to create a new instance. */ + @NonNull + static CqlSessionBuilder builder() { + return new CqlSessionBuilder(); + } + + /** + * Executes a CQL statement synchronously (the calling thread blocks until the result becomes + * available). + */ + @NonNull + default ResultSet execute(@NonNull Statement statement) { + return Objects.requireNonNull( + execute(statement, Statement.SYNC), "The CQL processor should never return a null result"); + } + + /** + * Executes a CQL statement synchronously (the calling thread blocks until the result becomes + * available). + */ + @NonNull + default ResultSet execute(@NonNull String query) { + return execute(SimpleStatement.newInstance(query)); + } + + /** + * Executes a CQL statement asynchronously (the call returns as soon as the statement was sent, + * generally before the result is available). + */ + @NonNull + default CompletionStage executeAsync(@NonNull Statement statement) { + return Objects.requireNonNull( + execute(statement, Statement.ASYNC), "The CQL processor should never return a null result"); + } + + /** + * Executes a CQL statement asynchronously (the call returns as soon as the statement was sent, + * generally before the result is available). + */ + @NonNull + default CompletionStage executeAsync(@NonNull String query) { + return executeAsync(SimpleStatement.newInstance(query)); + } + + /** + * Prepares a CQL statement synchronously (the calling thread blocks until the statement is + * prepared). + * + *

Note that the bound statements created from the resulting prepared statement will inherit + * some of the attributes of the provided simple statement. That is, given: + * + *

{@code
+   * SimpleStatement simpleStatement = SimpleStatement.newInstance("...");
+   * PreparedStatement preparedStatement = session.prepare(simpleStatement);
+   * BoundStatement boundStatement = preparedStatement.bind();
+   * }
+ * + * Then: + * + *
    + *
  • the following methods return the same value as their counterpart on {@code + * simpleStatement}: + *
      + *
    • {@link Request#getExecutionProfileName() boundStatement.getExecutionProfileName()} + *
    • {@link Request#getExecutionProfile() boundStatement.getExecutionProfile()} + *
    • {@link Statement#getPagingState() boundStatement.getPagingState()} + *
    • {@link Request#getRoutingKey() boundStatement.getRoutingKey()} + *
    • {@link Request#getRoutingToken() boundStatement.getRoutingToken()} + *
    • {@link Request#getCustomPayload() boundStatement.getCustomPayload()} + *
    • {@link Request#isIdempotent() boundStatement.isIdempotent()} + *
    • {@link Request#getTimeout() boundStatement.getTimeout()} + *
    • {@link Statement#getPagingState() boundStatement.getPagingState()} + *
    • {@link Statement#getPageSize() boundStatement.getPageSize()} + *
    • {@link Statement#getConsistencyLevel() boundStatement.getConsistencyLevel()} + *
    • {@link Statement#getSerialConsistencyLevel() + * boundStatement.getSerialConsistencyLevel()} + *
    • {@link Statement#isTracing() boundStatement.isTracing()} + *
    + *
  • {@link Request#getRoutingKeyspace() boundStatement.getRoutingKeyspace()} is set from + * either {@link Request#getKeyspace() simpleStatement.getKeyspace()} (if it's not {@code + * null}), or {@code simpleStatement.getRoutingKeyspace()}; + *
  • on the other hand, the following attributes are not propagated: + *
      + *
    • {@link Statement#getQueryTimestamp() boundStatement.getQueryTimestamp()} will be + * set to {@link Long#MIN_VALUE}, meaning that the value will be assigned by the + * session's timestamp generator. + *
    • {@link Statement#getNode() boundStatement.getNode()} will always be {@code null}. + *
    + *
+ * + * If you want to customize this behavior, you can write your own implementation of {@link + * PrepareRequest} and pass it to {@link #prepare(PrepareRequest)}. + * + *

The result of this method is cached: if you call it twice with the same {@link + * SimpleStatement}, you will get the same {@link PreparedStatement} instance. We still recommend + * keeping a reference to it (for example by caching it as a field in a DAO); if that's not + * possible (e.g. if query strings are generated dynamically), it's OK to call this method every + * time: there will just be a small performance overhead to check the internal cache. Note that + * caching is based on: + * + *

    + *
  • the query string exactly as you provided it: the driver does not perform any kind of + * trimming or sanitizing. + *
  • all other execution parameters: for example, preparing two statements with identical + * query strings but different {@linkplain SimpleStatement#getConsistencyLevel() consistency + * levels} will yield distinct prepared statements. + *
+ */ + @NonNull + default PreparedStatement prepare(@NonNull SimpleStatement statement) { + return Objects.requireNonNull( + execute(new DefaultPrepareRequest(statement), PrepareRequest.SYNC), + "The CQL prepare processor should never return a null result"); + } + + /** + * Prepares a CQL statement synchronously (the calling thread blocks until the statement is + * prepared). + * + *

The result of this method is cached (see {@link #prepare(SimpleStatement)} for more + * explanations). + */ + @NonNull + default PreparedStatement prepare(@NonNull String query) { + return Objects.requireNonNull( + execute(new DefaultPrepareRequest(query), PrepareRequest.SYNC), + "The CQL prepare processor should never return a null result"); + } + + /** + * Prepares a CQL statement synchronously (the calling thread blocks until the statement is + * prepared). + * + *

This variant is exposed in case you use an ad hoc {@link PrepareRequest} implementation to + * customize how attributes are propagated when you prepare a {@link SimpleStatement} (see {@link + * #prepare(SimpleStatement)} for more explanations). Otherwise, you should rarely have to deal + * with {@link PrepareRequest} directly. + * + *

The result of this method is cached (see {@link #prepare(SimpleStatement)} for more + * explanations). + */ + @NonNull + default PreparedStatement prepare(@NonNull PrepareRequest request) { + return Objects.requireNonNull( + execute(request, PrepareRequest.SYNC), + "The CQL prepare processor should never return a null result"); + } + + /** + * Prepares a CQL statement asynchronously (the call returns as soon as the prepare query was + * sent, generally before the statement is prepared). + * + *

Note that the bound statements created from the resulting prepared statement will inherit + * some of the attributes of {@code query}; see {@link #prepare(SimpleStatement)} for more + * details. + * + *

The result of this method is cached (see {@link #prepare(SimpleStatement)} for more + * explanations). + */ + @NonNull + default CompletionStage prepareAsync(@NonNull SimpleStatement statement) { + return Objects.requireNonNull( + execute(new DefaultPrepareRequest(statement), PrepareRequest.ASYNC), + "The CQL prepare processor should never return a null result"); + } + + /** + * Prepares a CQL statement asynchronously (the call returns as soon as the prepare query was + * sent, generally before the statement is prepared). + * + *

The result of this method is cached (see {@link #prepare(SimpleStatement)} for more + * explanations). + */ + @NonNull + default CompletionStage prepareAsync(@NonNull String query) { + return Objects.requireNonNull( + execute(new DefaultPrepareRequest(query), PrepareRequest.ASYNC), + "The CQL prepare processor should never return a null result"); + } + + /** + * Prepares a CQL statement asynchronously (the call returns as soon as the prepare query was + * sent, generally before the statement is prepared). + * + *

This variant is exposed in case you use an ad hoc {@link PrepareRequest} implementation to + * customize how attributes are propagated when you prepare a {@link SimpleStatement} (see {@link + * #prepare(SimpleStatement)} for more explanations). Otherwise, you should rarely have to deal + * with {@link PrepareRequest} directly. + * + *

The result of this method is cached (see {@link #prepare(SimpleStatement)} for more + * explanations). + */ + @NonNull + default CompletionStage prepareAsync(PrepareRequest request) { + return Objects.requireNonNull( + execute(request, PrepareRequest.ASYNC), + "The CQL prepare processor should never return a null result"); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/CqlSessionBuilder.java b/core/src/main/java/com/datastax/oss/driver/api/core/CqlSessionBuilder.java new file mode 100644 index 00000000000..064b6b12779 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/CqlSessionBuilder.java @@ -0,0 +1,30 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.NotThreadSafe; + +/** Helper class to build a {@link CqlSession} instance. */ +@NotThreadSafe +public class CqlSessionBuilder extends SessionBuilder { + + @Override + protected CqlSession wrap(@NonNull CqlSession defaultSession) { + return defaultSession; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/DefaultConsistencyLevel.java b/core/src/main/java/com/datastax/oss/driver/api/core/DefaultConsistencyLevel.java new file mode 100644 index 00000000000..34d8875eb8e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/DefaultConsistencyLevel.java @@ -0,0 +1,78 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; + +/** A default consistency level supported by the driver out of the box. */ +public enum DefaultConsistencyLevel implements ConsistencyLevel { + ANY(ProtocolConstants.ConsistencyLevel.ANY), + ONE(ProtocolConstants.ConsistencyLevel.ONE), + TWO(ProtocolConstants.ConsistencyLevel.TWO), + THREE(ProtocolConstants.ConsistencyLevel.THREE), + QUORUM(ProtocolConstants.ConsistencyLevel.QUORUM), + ALL(ProtocolConstants.ConsistencyLevel.ALL), + LOCAL_ONE(ProtocolConstants.ConsistencyLevel.LOCAL_ONE), + LOCAL_QUORUM(ProtocolConstants.ConsistencyLevel.LOCAL_QUORUM), + EACH_QUORUM(ProtocolConstants.ConsistencyLevel.EACH_QUORUM), + + SERIAL(ProtocolConstants.ConsistencyLevel.SERIAL), + LOCAL_SERIAL(ProtocolConstants.ConsistencyLevel.LOCAL_SERIAL), + ; + + private final int protocolCode; + + DefaultConsistencyLevel(int protocolCode) { + this.protocolCode = protocolCode; + } + + @Override + public int getProtocolCode() { + return protocolCode; + } + + @NonNull + public static DefaultConsistencyLevel fromCode(int code) { + DefaultConsistencyLevel level = BY_CODE.get(code); + if (level == null) { + throw new IllegalArgumentException("Unknown code: " + code); + } + return level; + } + + @Override + public boolean isDcLocal() { + return this == LOCAL_ONE || this == LOCAL_QUORUM || this == LOCAL_SERIAL; + } + + @Override + public boolean isSerial() { + return this == SERIAL || this == LOCAL_SERIAL; + } + + private static Map BY_CODE = mapByCode(values()); + + private static Map mapByCode(DefaultConsistencyLevel[] levels) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (DefaultConsistencyLevel level : levels) { + builder.put(level.protocolCode, level); + } + return builder.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/DefaultProtocolVersion.java b/core/src/main/java/com/datastax/oss/driver/api/core/DefaultProtocolVersion.java new file mode 100644 index 00000000000..feda0c2afc8 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/DefaultProtocolVersion.java @@ -0,0 +1,59 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.protocol.internal.ProtocolConstants; + +/** + * A protocol version supported by default by the driver. + * + *

Legacy versions 1 (Cassandra 1.2) and 2 (Cassandra 2.0) are not supported anymore. + */ +public enum DefaultProtocolVersion implements ProtocolVersion { + + /** Version 3, supported by Cassandra 2.1 and above. */ + V3(ProtocolConstants.Version.V3, false), + + /** Version 4, supported by Cassandra 2.2 and above. */ + V4(ProtocolConstants.Version.V4, false), + + /** + * Version 5, currently supported as a beta preview in Cassandra 3.10 and above. + * + *

Do not use this in production. + * + * @see ProtocolVersion#isBeta() + */ + V5(ProtocolConstants.Version.V5, true); + + private final int code; + private final boolean beta; + + DefaultProtocolVersion(int code, boolean beta) { + this.code = code; + this.beta = beta; + } + + @Override + public int getCode() { + return code; + } + + @Override + public boolean isBeta() { + return beta; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/DriverException.java b/core/src/main/java/com/datastax/oss/driver/api/core/DriverException.java new file mode 100644 index 00000000000..07f79d6e341 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/DriverException.java @@ -0,0 +1,104 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Base class for all exceptions thrown by the driver. + * + *

Note that, for obvious programming errors, the driver might throw JDK runtime exceptions, such + * as {@link IllegalArgumentException} or {@link IllegalStateException}. In all other cases, it will + * be an instance of this class. + * + *

One special case is when the driver tried multiple nodes to complete a request, and they all + * failed; the error returned to the client will be an {@link AllNodesFailedException}, which wraps + * a map of errors per node. + * + *

Some implementations make the stack trace not writable to improve performance (see {@link + * Throwable#Throwable(String, Throwable, boolean, boolean)}). This is only done when the exception + * is thrown in a small number of well-known cases, and the stack trace wouldn't add any useful + * information (for example, server error responses). Instances returned by {@link #copy()} always + * have a stack trace. + */ +public abstract class DriverException extends RuntimeException { + + private volatile ExecutionInfo executionInfo; + + protected DriverException( + @Nullable String message, + @Nullable ExecutionInfo executionInfo, + @Nullable Throwable cause, + boolean writableStackTrace) { + super(message, cause, true, writableStackTrace); + this.executionInfo = executionInfo; + } + + /** + * Returns execution information about the request that led to this error. + * + *

This is similar to the information returned for a successful query in {@link ResultSet}, + * except that some fields may be absent: + * + *

    + *
  • {@link ExecutionInfo#getCoordinator()} may be null if the error occurred before any node + * was contacted; + *
  • {@link ExecutionInfo#getErrors()} will contain the errors encountered for other nodes, + * but not this error itself; + *
  • {@link ExecutionInfo#getSuccessfulExecutionIndex()} may be -1 if the error occurred + * before any execution was started; + *
  • {@link ExecutionInfo#getPagingState()} and {@link ExecutionInfo#getTracingId()} will + * always be null; + *
  • {@link ExecutionInfo#getWarnings()} and {@link ExecutionInfo#getIncomingPayload()} will + * always be empty; + *
  • {@link ExecutionInfo#isSchemaInAgreement()} will always be true; + *
  • {@link ExecutionInfo#getResponseSizeInBytes()} and {@link + * ExecutionInfo#getCompressedResponseSizeInBytes()} will always be -1. + *
+ * + *

Note that this is only set for exceptions that are rethrown directly to the client from a + * session call. For example, individual node errors stored in {@link + * AllNodesFailedException#getErrors()} or {@link ExecutionInfo#getErrors()} do not contain their + * own execution info, and therefore return null from this method. + */ + public ExecutionInfo getExecutionInfo() { + return executionInfo; + } + + /** This is for internal use by the driver, a client application has no reason to call it. */ + public void setExecutionInfo(ExecutionInfo executionInfo) { + this.executionInfo = executionInfo; + } + + /** + * Copy the exception. + * + *

This returns a new exception, equivalent to the original one, except that because a new + * object is created in the current thread, the top-most element in the stacktrace of the + * exception will refer to the current thread. The original exception may or may not be included + * as the copy's cause, depending on whether that is deemed useful (this is left to the discretion + * of each implementation). + * + *

This is intended for the synchronous wrapper methods of the driver, in order to produce a + * more user-friendly stack trace (that includes the line in the user code where the driver + * rethrew the error). + */ + @NonNull + public abstract DriverException copy(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/DriverExecutionException.java b/core/src/main/java/com/datastax/oss/driver/api/core/DriverExecutionException.java new file mode 100644 index 00000000000..e7d8e42f2b1 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/DriverExecutionException.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Statement; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Thrown by synchronous wrapper methods (such as {@link CqlSession#execute(Statement)}, when the + * underlying future was completed with a checked exception. + * + *

This exception should be rarely thrown (if ever). Most of the time, the driver uses unchecked + * exceptions, which will be rethrown directly instead of being wrapped in this class. + */ +public class DriverExecutionException extends DriverException { + public DriverExecutionException(Throwable cause) { + this(null, cause); + } + + private DriverExecutionException(ExecutionInfo executionInfo, Throwable cause) { + super(null, executionInfo, cause, true); + } + + @NonNull + @Override + public DriverException copy() { + return new DriverExecutionException(getExecutionInfo(), getCause()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/DriverTimeoutException.java b/core/src/main/java/com/datastax/oss/driver/api/core/DriverTimeoutException.java new file mode 100644 index 00000000000..1966606256b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/DriverTimeoutException.java @@ -0,0 +1,36 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** Thrown when a driver request timed out. */ +public class DriverTimeoutException extends DriverException { + public DriverTimeoutException(@NonNull String message) { + this(message, null); + } + + private DriverTimeoutException(String message, ExecutionInfo executionInfo) { + super(message, executionInfo, null, true); + } + + @NonNull + @Override + public DriverException copy() { + return new DriverTimeoutException(getMessage(), getExecutionInfo()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/InvalidKeyspaceException.java b/core/src/main/java/com/datastax/oss/driver/api/core/InvalidKeyspaceException.java new file mode 100644 index 00000000000..0b1ec172812 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/InvalidKeyspaceException.java @@ -0,0 +1,36 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** Thrown when a session gets created with an invalid keyspace. */ +public class InvalidKeyspaceException extends DriverException { + public InvalidKeyspaceException(@NonNull String message) { + this(message, null); + } + + private InvalidKeyspaceException(String message, ExecutionInfo executionInfo) { + super(message, executionInfo, null, true); + } + + @NonNull + @Override + public DriverException copy() { + return new InvalidKeyspaceException(getMessage(), getExecutionInfo()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/MappedAsyncPagingIterable.java b/core/src/main/java/com/datastax/oss/driver/api/core/MappedAsyncPagingIterable.java new file mode 100644 index 00000000000..c6cb7ae5831 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/MappedAsyncPagingIterable.java @@ -0,0 +1,22 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import java.util.function.Function; + +/** The result of calling {@link #map(Function)} on another async iterable. */ +public interface MappedAsyncPagingIterable + extends AsyncPagingIterable> {} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/MavenCoordinates.java b/core/src/main/java/com/datastax/oss/driver/api/core/MavenCoordinates.java new file mode 100644 index 00000000000..ab26be868dd --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/MavenCoordinates.java @@ -0,0 +1,33 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import edu.umd.cs.findbugs.annotations.NonNull; + +public interface MavenCoordinates { + + @NonNull + String getGroupId(); + + @NonNull + String getArtifactId(); + + @NonNull + Version getVersion(); + + @NonNull + String getName(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/NoNodeAvailableException.java b/core/src/main/java/com/datastax/oss/driver/api/core/NoNodeAvailableException.java new file mode 100644 index 00000000000..db231adf219 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/NoNodeAvailableException.java @@ -0,0 +1,42 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Collections; + +/** + * Specialization of {@code AllNodesFailedException} when no coordinators were tried. + * + *

This can happen if all nodes are down, or if all the contact points provided at startup were + * invalid. + */ +public class NoNodeAvailableException extends AllNodesFailedException { + public NoNodeAvailableException() { + this(null); + } + + private NoNodeAvailableException(ExecutionInfo executionInfo) { + super("No node was available to execute the query", executionInfo, Collections.emptyMap()); + } + + @NonNull + @Override + public DriverException copy() { + return new NoNodeAvailableException(getExecutionInfo()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/PagingIterable.java b/core/src/main/java/com/datastax/oss/driver/api/core/PagingIterable.java new file mode 100644 index 00000000000..0a7f4768de4 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/PagingIterable.java @@ -0,0 +1,161 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.internal.core.PagingIterableWrapper; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterables; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + +/** + * An iterable of elements which are fetched synchronously by the driver, possibly in multiple + * requests. + * + *

It uses asynchronous calls internally, but blocks on the results in order to provide a + * synchronous API to its clients. If the query is paged, only the first page will be fetched + * initially, and iteration will trigger background fetches of the next pages when necessary. + * + *

Note that this object can only be iterated once: elements are "consumed" as they are read, + * subsequent calls to {@code iterator()} will return the same iterator instance. + * + *

Implementations of this type are not thread-safe. They can only be iterated by the + * thread that invoked {@code session.execute}. + * + *

This is a generalization of {@link ResultSet}, replacing rows by an arbitrary element type. + */ +public interface PagingIterable extends Iterable { + + /** Metadata about the columns returned by the CQL request that was used to build this result. */ + @NonNull + ColumnDefinitions getColumnDefinitions(); + + /** + * The execution information for the last query performed for this iterable. + * + *

This is a shortcut for: + * + *

+   * getExecutionInfos().get(getExecutionInfos().size() - 1)
+   * 
+ * + * @see #getExecutionInfos() + */ + @NonNull + default ExecutionInfo getExecutionInfo() { + List infos = getExecutionInfos(); + return infos.get(infos.size() - 1); + } + + /** + * The execution information for all the queries that have been performed so far to assemble this + * iterable. + * + *

This will have multiple elements if the query is paged, since the driver performs blocking + * background queries to fetch additional pages transparently as the result set is being iterated. + */ + @NonNull + List getExecutionInfos(); + + /** + * Returns the next element, or {@code null} if the iterable is exhausted. + * + *

This is convenient for queries that are known to return exactly one row, for example count + * queries. + */ + @Nullable + default ElementT one() { + Iterator iterator = iterator(); + return iterator.hasNext() ? iterator.next() : null; + } + + /** + * Returns all the remaining elements as a list; not recommended for queries that return a + * large number of elements. + * + *

Contrary to {@link #iterator()} or successive calls to {@link #one()}, this method forces + * fetching the full contents at once; in particular, this means that a large number of + * background queries might have to be run, and that all the data will be held in memory locally. + * Therefore it is crucial to only call this method for queries that are known to return a + * reasonable number of results. + */ + @NonNull + default List all() { + if (!iterator().hasNext()) { + return Collections.emptyList(); + } + // We can't know the actual size in advance since more pages could be fetched, but we can at + // least allocate for what we already have. + List result = Lists.newArrayListWithExpectedSize(getAvailableWithoutFetching()); + Iterables.addAll(result, this); + return result; + } + + /** + * Whether all pages have been fetched from the database. + * + *

If this is {@code false}, it means that more blocking background queries will be triggered + * as iteration continues. + */ + boolean isFullyFetched(); + + /** + * The number of elements that can be returned from this result set before a blocking background + * query needs to be performed to retrieve more results. In other words, this is the number of + * elements remaining in the current page. + * + *

This is useful if you use the paging state to pause the iteration and resume it later: after + * you've retrieved the state ({@link ExecutionInfo#getPagingState() + * getExecutionInfo().getPagingState()}), call this method and iterate the remaining elements; + * that way you're not leaving a gap between the last element and the position you'll restart from + * when you reinject the state in a new query. + */ + int getAvailableWithoutFetching(); + + /** + * If the query that produced this result was a CQL conditional update, indicate whether it was + * successfully applied. + * + *

For consistency, this method always returns {@code true} for non-conditional queries + * (although there is no reason to call the method in that case). This is also the case for + * conditional DDL statements ({@code CREATE KEYSPACE... IF NOT EXISTS}, {@code CREATE TABLE... IF + * NOT EXISTS}), for which Cassandra doesn't return an {@code [applied]} column. + * + *

Note that, for versions of Cassandra strictly lower than 2.1.0-rc2, a server-side bug (CASSANDRA-7337) causes this + * method to always return {@code true} for batches containing conditional queries. + */ + boolean wasApplied(); + + /** + * Creates a new instance by transforming each element of this iterable with the provided + * function. + * + *

Note that both instances share the same underlying data: consuming elements from the + * transformed iterable will also consume them from this object, and vice-versa. + */ + default PagingIterable map( + Function elementMapper) { + return new PagingIterableWrapper<>(this, elementMapper); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/ProtocolVersion.java b/core/src/main/java/com/datastax/oss/driver/api/core/ProtocolVersion.java new file mode 100644 index 00000000000..e39837cc090 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/ProtocolVersion.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.detach.Detachable; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A version of the native protocol used by the driver to communicate with the server. + * + *

The only reason to model this as an interface (as opposed to an enum type) is to accommodate + * for custom protocol extensions. If you're connecting to a standard Apache Cassandra cluster, all + * {@code ProtocolVersion}s are {@link DefaultProtocolVersion} instances. + */ +public interface ProtocolVersion { + /** The default version used for {@link Detachable detached} objects. */ + // Implementation note: we can't use the ProtocolVersionRegistry here, this has to be a + // compile-time constant. + ProtocolVersion DEFAULT = DefaultProtocolVersion.V4; + + /** + * A numeric code that uniquely identifies the version (this is the code used in network frames). + */ + int getCode(); + + /** A string representation of the version. */ + @NonNull + String name(); + + /** + * Whether the protocol version is in a beta status. + * + *

Beta versions are intended for Cassandra development. They should not be used in a regular + * application, as beta features may break at any point. + */ + boolean isBeta(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/RequestThrottlingException.java b/core/src/main/java/com/datastax/oss/driver/api/core/RequestThrottlingException.java new file mode 100644 index 00000000000..5bd44fcb1d2 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/RequestThrottlingException.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Thrown if the session uses a request throttler, and it didn't allow the current request to + * execute. + * + *

This can happen either when the session is overloaded, or at shutdown for requests that had + * been enqueued. + */ +public class RequestThrottlingException extends DriverException { + + public RequestThrottlingException(@NonNull String message) { + this(message, null); + } + + private RequestThrottlingException(String message, ExecutionInfo executionInfo) { + super(message, executionInfo, null, true); + } + + @NonNull + @Override + public DriverException copy() { + return new RequestThrottlingException(getMessage(), getExecutionInfo()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/UnsupportedProtocolVersionException.java b/core/src/main/java/com/datastax/oss/driver/api/core/UnsupportedProtocolVersionException.java new file mode 100644 index 00000000000..d80eba55514 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/UnsupportedProtocolVersionException.java @@ -0,0 +1,96 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Collections; +import java.util.List; + +/** + * Indicates that we've attempted to connect to a Cassandra node with a protocol version that it + * cannot handle (e.g., connecting to a C* 2.1 node with protocol version 4). + * + *

The only time when this is returned directly to the client (wrapped in a {@link + * AllNodesFailedException}) is at initialization. If it happens later when the driver is already + * connected, it is just logged an the corresponding node is forced down. + */ +public class UnsupportedProtocolVersionException extends DriverException { + private static final long serialVersionUID = 0; + + private final EndPoint endPoint; + private final List attemptedVersions; + + @NonNull + public static UnsupportedProtocolVersionException forSingleAttempt( + @NonNull EndPoint endPoint, @NonNull ProtocolVersion attemptedVersion) { + String message = + String.format("[%s] Host does not support protocol version %s", endPoint, attemptedVersion); + return new UnsupportedProtocolVersionException( + endPoint, message, Collections.singletonList(attemptedVersion), null); + } + + @NonNull + public static UnsupportedProtocolVersionException forNegotiation( + @NonNull EndPoint endPoint, @NonNull List attemptedVersions) { + String message = + String.format( + "[%s] Protocol negotiation failed: could not find a common version (attempted: %s). " + + "Note that the driver does not support Cassandra 2.0 or lower.", + endPoint, attemptedVersions); + return new UnsupportedProtocolVersionException( + endPoint, message, ImmutableList.copyOf(attemptedVersions), null); + } + + public UnsupportedProtocolVersionException( + @Nullable EndPoint endPoint, // technically nullable, but should never be in real life + @NonNull String message, + @NonNull List attemptedVersions) { + this(endPoint, message, attemptedVersions, null); + } + + private UnsupportedProtocolVersionException( + EndPoint endPoint, + String message, + List attemptedVersions, + ExecutionInfo executionInfo) { + super(message, executionInfo, null, true); + this.endPoint = endPoint; + this.attemptedVersions = attemptedVersions; + } + + /** The address of the node that threw the error. */ + @Nullable + public EndPoint getEndPoint() { + return endPoint; + } + + /** The versions that were attempted. */ + @NonNull + public List getAttemptedVersions() { + return attemptedVersions; + } + + @NonNull + @Override + public DriverException copy() { + return new UnsupportedProtocolVersionException( + endPoint, getMessage(), attemptedVersions, getExecutionInfo()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/Version.java b/core/src/main/java/com/datastax/oss/driver/api/core/Version.java new file mode 100644 index 00000000000..f70db10c252 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/Version.java @@ -0,0 +1,300 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.jcip.annotations.Immutable; + +/** + * A structured version number. + * + *

It is in the form X.Y.Z, with optional pre-release labels and build metadata. + * + *

Version numbers compare the usual way, the major number (X) is compared first, then the minor + * one (Y) and then the patch level one (Z). Lastly, versions with pre-release sorts before the + * versions that don't have one, and labels are sorted alphabetically if necessary. Build metadata + * are ignored for sorting versions. + */ +@Immutable +public class Version implements Comparable { + + private static final String VERSION_REGEXP = + "(\\d+)\\.(\\d+)(\\.\\d+)?(\\.\\d+)?([~\\-]\\w[.\\w]*(?:\\-\\w[.\\w]*)*)?(\\+[.\\w]+)?"; + private static final Pattern pattern = Pattern.compile(VERSION_REGEXP); + + public static final Version V2_1_0 = parse("2.1.0"); + public static final Version V2_2_0 = parse("2.2.0"); + public static final Version V3_0_0 = parse("3.0.0"); + public static final Version V4_0_0 = parse("4.0.0"); + + private final int major; + private final int minor; + private final int patch; + private final int dsePatch; + + private final String[] preReleases; + private final String build; + + private Version( + int major, int minor, int patch, int dsePatch, String[] preReleases, String build) { + this.major = major; + this.minor = minor; + this.patch = patch; + this.dsePatch = dsePatch; + this.preReleases = preReleases; + this.build = build; + } + + /** + * Parses a version from a string. + * + *

The version string should have primarily the form X.Y.Z to which can be appended one or more + * pre-release label after dashes (2.0.1-beta1, 2.1.4-rc1-SNAPSHOT) and an optional build label + * (2.1.0-beta1+a20ba.sha). Out of convenience, the "patch" version number, Z, can be omitted, in + * which case it is assumed to be 0. + * + * @param version the string to parse. + * @return the parsed version number. + * @throws IllegalArgumentException if the provided string does not represent a valid version. + */ + @Nullable + public static Version parse(@Nullable String version) { + if (version == null) { + return null; + } + + Matcher matcher = pattern.matcher(version); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid version number: " + version); + } + + try { + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + + String pa = matcher.group(3); + int patch = + pa == null || pa.isEmpty() + ? 0 + : Integer.parseInt( + pa.substring(1)); // dropping the initial '.' since it's included this time + + String dse = matcher.group(4); + int dsePatch = + dse == null || dse.isEmpty() + ? -1 + : Integer.parseInt( + dse.substring(1)); // dropping the initial '.' since it's included this time + + String pr = matcher.group(5); + String[] preReleases = + pr == null || pr.isEmpty() + ? null + : pr.substring(1) + .split("\\-"); // drop initial '-' or '~' then split on the remaining ones + + String bl = matcher.group(6); + String build = bl == null || bl.isEmpty() ? null : bl.substring(1); // drop the initial '+' + + return new Version(major, minor, patch, dsePatch, preReleases, build); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid version number: " + version); + } + } + + /** + * The major version number. + * + * @return the major version number, i.e. X in X.Y.Z. + */ + public int getMajor() { + return major; + } + + /** + * The minor version number. + * + * @return the minor version number, i.e. Y in X.Y.Z. + */ + public int getMinor() { + return minor; + } + + /** + * The patch version number. + * + * @return the patch version number, i.e. Z in X.Y.Z. + */ + public int getPatch() { + return patch; + } + + /** + * The DSE patch version number (will only be present for version of Cassandra in DSE). + * + *

DataStax Entreprise (DSE) adds a fourth number to the version number to track potential hot + * fixes and/or DSE specific patches that may have been applied to the Cassandra version. In that + * case, this method returns that fourth number. + * + * @return the DSE patch version number, i.e. D in X.Y.Z.D, or -1 if the version number is not + * from DSE. + */ + public int getDSEPatch() { + return dsePatch; + } + + /** + * The pre-release labels if relevant, i.e. label1 and label2 in X.Y.Z-label1-lable2. + * + * @return the pre-release labels. The return list will be {@code null} if the version number + * doesn't have any. + */ + public List getPreReleaseLabels() { + return preReleases == null ? null : Collections.unmodifiableList(Arrays.asList(preReleases)); + } + + /** + * The build label if there is one. + * + * @return the build label or {@code null} if the version number doesn't have one. + */ + public String getBuildLabel() { + return build; + } + + /** + * The next stable version, i.e. the version stripped of its pre-release labels and build + * metadata. + * + *

This is mostly used during our development stage, where we test the driver against + * pre-release versions of Cassandra like 2.1.0-rc7-SNAPSHOT, but need to compare to the stable + * version 2.1.0 when testing for native protocol compatibility, etc. + * + * @return the next stable version. + */ + public Version nextStable() { + return new Version(major, minor, patch, dsePatch, null, null); + } + + @Override + public int compareTo(@NonNull Version other) { + if (major < other.major) { + return -1; + } + if (major > other.major) { + return 1; + } + + if (minor < other.minor) { + return -1; + } + if (minor > other.minor) { + return 1; + } + + if (patch < other.patch) { + return -1; + } + if (patch > other.patch) { + return 1; + } + + if (dsePatch < 0) { + if (other.dsePatch >= 0) { + return -1; + } + } else { + if (other.dsePatch < 0) { + return 1; + } + + // Both are >= 0 + if (dsePatch < other.dsePatch) { + return -1; + } + if (dsePatch > other.dsePatch) { + return 1; + } + } + + if (preReleases == null) { + return other.preReleases == null ? 0 : 1; + } + if (other.preReleases == null) { + return -1; + } + + for (int i = 0; i < Math.min(preReleases.length, other.preReleases.length); i++) { + int cmp = preReleases[i].compareTo(other.preReleases[i]); + if (cmp != 0) { + return cmp; + } + } + + return preReleases.length == other.preReleases.length + ? 0 + : (preReleases.length < other.preReleases.length ? -1 : 1); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof Version) { + Version that = (Version) other; + return this.major == that.major + && this.minor == that.minor + && this.patch == that.patch + && this.dsePatch == that.dsePatch + && (this.preReleases == null + ? that.preReleases == null + : Arrays.equals(this.preReleases, that.preReleases)) + && Objects.equals(this.build, that.build); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(major, minor, patch, dsePatch, Arrays.hashCode(preReleases), build); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(major).append('.').append(minor).append('.').append(patch); + if (dsePatch >= 0) { + sb.append('.').append(dsePatch); + } + if (preReleases != null) { + for (String preRelease : preReleases) { + sb.append('-').append(preRelease); + } + } + if (build != null) { + sb.append('+').append(build); + } + return sb.toString(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/addresstranslation/AddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/api/core/addresstranslation/AddressTranslator.java new file mode 100644 index 00000000000..c80e16d3363 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/addresstranslation/AddressTranslator.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.addresstranslation; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetSocketAddress; + +/** + * Translates IP addresses received from Cassandra nodes into locally queriable addresses. + * + *

The driver auto-detects new Cassandra nodes added to the cluster through server side pushed + * notifications and system table queries. For each node, the address the driver will receive will + * correspond to the address set as {@code broadcast_rpc_address} in the node's YAML file. In most + * cases, this is the correct address to use by the driver, and that is what is used by default. + * However, sometimes the addresses received through this mechanism will either not be reachable + * directly by the driver, or should not be the preferred address to use to reach the node (for + * instance, the {@code broadcast_rpc_address} set on Cassandra nodes might be a private IP, but + * some clients may have to use a public IP, or go through a router to reach that node). This + * interface addresses such cases, by allowing to translate an address as sent by a Cassandra node + * into another address to be used by the driver for connection. + * + *

The contact point addresses provided at driver initialization are considered translated + * already; in other words, they will be used as-is, without being processed by this component. + */ +public interface AddressTranslator extends AutoCloseable { + + /** + * Translates an address reported by a Cassandra node into the address that the driver will use to + * connect. + */ + @NonNull + InetSocketAddress translate(@NonNull InetSocketAddress address); + + /** Called when the cluster that this translator is associated with closes. */ + @Override + void close(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/auth/AuthProvider.java b/core/src/main/java/com/datastax/oss/driver/api/core/auth/AuthProvider.java new file mode 100644 index 00000000000..e85b90e3b04 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/auth/AuthProvider.java @@ -0,0 +1,59 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.auth; + +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.internal.core.auth.PlainTextAuthProvider; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Provides {@link Authenticator} instances to use when connecting to Cassandra nodes. + * + *

See {@link PlainTextAuthProvider} for an implementation which uses SASL PLAIN mechanism to + * authenticate using username/password strings. + */ +public interface AuthProvider extends AutoCloseable { + + /** + * The authenticator to use when connecting to {@code host}. + * + * @param endPoint the Cassandra host to connect to. + * @param serverAuthenticator the configured authenticator on the host. + * @return the authentication implementation to use. + */ + @NonNull + Authenticator newAuthenticator(@NonNull EndPoint endPoint, @NonNull String serverAuthenticator) + throws AuthenticationException; + + /** + * What to do if the server does not send back an authentication challenge (in other words, lets + * the client connect without any form of authentication). + * + *

This is suspicious because having authentication enabled on the client but not on the server + * is probably a configuration mistake. + * + *

Provider implementations are free to handle this however they want; typical approaches are: + * + *

    + *
  • ignoring; + *
  • logging a warning; + *
  • throwing an {@link AuthenticationException} to abort the connection (but note that it + * will be retried according to the {@link ReconnectionPolicy}). + *
+ */ + void onMissingChallenge(@NonNull EndPoint endPoint) throws AuthenticationException; +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/auth/AuthenticationException.java b/core/src/main/java/com/datastax/oss/driver/api/core/auth/AuthenticationException.java new file mode 100644 index 00000000000..abf77e293d5 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/auth/AuthenticationException.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.auth; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Indicates an error during the authentication phase while connecting to a node. + * + *

The only time when this is returned directly to the client (wrapped in a {@link + * AllNodesFailedException}) is at initialization. If it happens later when the driver is already + * connected, it is just logged and the connection will be reattempted. + */ +public class AuthenticationException extends RuntimeException { + private static final long serialVersionUID = 0; + + private final EndPoint endPoint; + + public AuthenticationException(@NonNull EndPoint endPoint, @NonNull String message) { + this(endPoint, message, null); + } + + public AuthenticationException( + @NonNull EndPoint endPoint, @NonNull String message, @Nullable Throwable cause) { + super(String.format("Authentication error on node %s: %s", endPoint, message), cause); + this.endPoint = endPoint; + } + + /** The address of the node that encountered the error. */ + @NonNull + public EndPoint getEndPoint() { + return endPoint; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/auth/Authenticator.java b/core/src/main/java/com/datastax/oss/driver/api/core/auth/Authenticator.java new file mode 100644 index 00000000000..dd92762577e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/auth/Authenticator.java @@ -0,0 +1,88 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.auth; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletionStage; + +/** + * Handles SASL authentication with Cassandra servers. + * + *

Each time a new connection is created and the server requires authentication, a new instance + * of this class will be created by the corresponding {@link AuthProvider} to handle that + * authentication. The lifecycle of that new {@code Authenticator} will be: + * + *

    + *
  1. the {@link #initialResponse} method will be called. The initial return value will be sent + * to the server to initiate the handshake. + *
  2. the server will respond to each client response by either issuing a challenge or indicating + * that the authentication is complete (successfully or not). If a new challenge is issued, + * the authenticator's {@link #evaluateChallenge} method will be called to produce a response + * that will be sent to the server. This challenge/response negotiation will continue until + * the server responds that authentication is successful (or an {@link + * AuthenticationException} is raised). + *
  3. When the server indicates that authentication is successful, the {@link + * #onAuthenticationSuccess} method will be called with the last information that the server + * may optionally have sent. + *
+ * + * The exact nature of the negotiation between client and server is specific to the authentication + * mechanism configured server side. + * + *

Note that, since the methods in this interface will be invoked on a driver I/O thread, they + * all return asynchronous results. If your implementation performs heavy computations or blocking + * calls, you'll want to schedule them on a separate executor, and return a {@code CompletionStage} + * that represents their future completion. If your implementation is fast, lightweight and does not + * perform blocking operations, it might be acceptable to run it on I/O threads directly; in that + * case, implement {@link SyncAuthenticator} instead of this interface. + */ +public interface Authenticator { + + /** + * Obtain an initial response token for initializing the SASL handshake. + * + * @return a completion stage that will complete with the initial response to send to the server + * (which may be {@code null}). + */ + @NonNull + CompletionStage initialResponse(); + + /** + * Evaluate a challenge received from the server. Generally, this method should return null when + * authentication is complete from the client perspective. + * + * @param challenge the server's SASL challenge. + * @return a completion stage that will complete with the updated SASL token (which may be null to + * indicate the client requires no further action). + */ + @NonNull + CompletionStage evaluateChallenge(@Nullable ByteBuffer challenge); + + /** + * Called when authentication is successful with the last information optionally sent by the + * server. + * + * @param token the information sent by the server with the authentication successful message. + * This will be {@code null} if the server sends no particular information on authentication + * success. + * @return a completion stage that completes when the authenticator is done processing this + * response. + */ + @NonNull + CompletionStage onAuthenticationSuccess(@Nullable ByteBuffer token); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/auth/SyncAuthenticator.java b/core/src/main/java/com/datastax/oss/driver/api/core/auth/SyncAuthenticator.java new file mode 100644 index 00000000000..d2d1d5d5f3b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/auth/SyncAuthenticator.java @@ -0,0 +1,90 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.auth; + +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletionStage; + +/** + * An authenticator that performs all of its operations synchronously, on the calling thread. + * + *

This is intended for simple implementations that are fast and lightweight enough, and do not + * perform any blocking operations. + */ +public interface SyncAuthenticator extends Authenticator { + + /** + * Obtain an initial response token for initializing the SASL handshake. + * + *

{@link #initialResponse()} calls this and wraps the result in an immediately completed + * future. + * + * @return The initial response to send to the server (which may be {@code null}). + */ + @Nullable + ByteBuffer initialResponseSync(); + + /** + * Evaluate a challenge received from the server. + * + *

{@link #evaluateChallenge(ByteBuffer)} calls this and wraps the result in an immediately + * completed future. + * + * @param challenge the server's SASL challenge; may be {@code null}. + * @return The updated SASL token (which may be {@code null} to indicate the client requires no + * further action). + */ + @Nullable + ByteBuffer evaluateChallengeSync(@Nullable ByteBuffer challenge); + + /** + * Called when authentication is successful with the last information optionally sent by the + * server. + * + *

{@link #onAuthenticationSuccess(ByteBuffer)} calls this, and then returns an immediately + * completed future. + * + * @param token the information sent by the server with the authentication successful message. + * This will be {@code null} if the server sends no particular information on authentication + * success. + */ + void onAuthenticationSuccessSync(@Nullable ByteBuffer token); + + @NonNull + @Override + default CompletionStage initialResponse() { + return CompletableFutures.wrap(this::initialResponseSync); + } + + @NonNull + @Override + default CompletionStage evaluateChallenge(@Nullable ByteBuffer challenge) { + return CompletableFutures.wrap(() -> evaluateChallengeSync(challenge)); + } + + @NonNull + @Override + default CompletionStage onAuthenticationSuccess(@Nullable ByteBuffer token) { + return CompletableFutures.wrap( + () -> { + onAuthenticationSuccessSync(token); + return null; + }); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/auth/package-info.java b/core/src/main/java/com/datastax/oss/driver/api/core/auth/package-info.java new file mode 100644 index 00000000000..d5d4efd9c9d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/auth/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Support for authentication between the driver and Cassandra nodes. + * + *

Authentication is performed on each newly open connection. It is customizable via the {@link + * com.datastax.oss.driver.api.core.auth.AuthProvider} interface. + */ +package com.datastax.oss.driver.api.core.auth; diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java new file mode 100644 index 00000000000..89d8365de78 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java @@ -0,0 +1,189 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Built-in driver options for the core driver. + * + *

Refer to {@code reference.conf} in the driver codebase for a full description of each option. + */ +public enum DefaultDriverOption implements DriverOption { + CONTACT_POINTS("basic.contact-points"), + SESSION_NAME("basic.session-name"), + SESSION_KEYSPACE("basic.session-keyspace"), + CONFIG_RELOAD_INTERVAL("basic.config-reload-interval"), + + REQUEST_TIMEOUT("basic.request.timeout"), + REQUEST_CONSISTENCY("basic.request.consistency"), + REQUEST_PAGE_SIZE("basic.request.page-size"), + REQUEST_SERIAL_CONSISTENCY("basic.request.serial-consistency"), + REQUEST_DEFAULT_IDEMPOTENCE("basic.request.default-idempotence"), + + LOAD_BALANCING_POLICY("basic.load-balancing-policy"), + LOAD_BALANCING_POLICY_CLASS("basic.load-balancing-policy.class"), + LOAD_BALANCING_LOCAL_DATACENTER("basic.load-balancing-policy.local-datacenter"), + LOAD_BALANCING_FILTER_CLASS("basic.load-balancing-policy.filter.class"), + + CONNECTION_INIT_QUERY_TIMEOUT("advanced.connection.init-query-timeout"), + CONNECTION_SET_KEYSPACE_TIMEOUT("advanced.connection.set-keyspace-timeout"), + CONNECTION_MAX_REQUESTS("advanced.connection.max-requests-per-connection"), + CONNECTION_MAX_ORPHAN_REQUESTS("advanced.connection.max-orphan-requests"), + CONNECTION_WARN_INIT_ERROR("advanced.connection.warn-on-init-error"), + CONNECTION_POOL_LOCAL_SIZE("advanced.connection.pool.local.size"), + CONNECTION_POOL_REMOTE_SIZE("advanced.connection.pool.remote.size"), + + RECONNECT_ON_INIT("advanced.reconnect-on-init"), + + RECONNECTION_POLICY_CLASS("advanced.reconnection-policy.class"), + RECONNECTION_BASE_DELAY("advanced.reconnection-policy.base-delay"), + RECONNECTION_MAX_DELAY("advanced.reconnection-policy.max-delay"), + + RETRY_POLICY("advanced.retry-policy"), + RETRY_POLICY_CLASS("advanced.retry-policy.class"), + + SPECULATIVE_EXECUTION_POLICY("advanced.speculative-execution-policy"), + SPECULATIVE_EXECUTION_POLICY_CLASS("advanced.speculative-execution-policy.class"), + SPECULATIVE_EXECUTION_MAX("advanced.speculative-execution-policy.max-executions"), + SPECULATIVE_EXECUTION_DELAY("advanced.speculative-execution-policy.delay"), + + AUTH_PROVIDER_CLASS("advanced.auth-provider.class"), + AUTH_PROVIDER_USER_NAME("advanced.auth-provider.username"), + AUTH_PROVIDER_PASSWORD("advanced.auth-provider.password"), + + SSL_ENGINE_FACTORY_CLASS("advanced.ssl-engine-factory.class"), + SSL_CIPHER_SUITES("advanced.ssl-engine-factory.cipher-suites"), + SSL_HOSTNAME_VALIDATION("advanced.ssl-engine-factory.hostname-validation"), + SSL_KEYSTORE_PATH("advanced.ssl-engine-factory.keystore-path"), + SSL_KEYSTORE_PASSWORD("advanced.ssl-engine-factory.keystore-password"), + SSL_TRUSTSTORE_PATH("advanced.ssl-engine-factory.truststore-path"), + SSL_TRUSTSTORE_PASSWORD("advanced.ssl-engine-factory.truststore-password"), + + TIMESTAMP_GENERATOR_CLASS("advanced.timestamp-generator.class"), + TIMESTAMP_GENERATOR_FORCE_JAVA_CLOCK("advanced.timestamp-generator.force-java-clock"), + TIMESTAMP_GENERATOR_DRIFT_WARNING_THRESHOLD( + "advanced.timestamp-generator.drift-warning.threshold"), + TIMESTAMP_GENERATOR_DRIFT_WARNING_INTERVAL("advanced.timestamp-generator.drift-warning.interval"), + + REQUEST_TRACKER_CLASS("advanced.request-tracker.class"), + REQUEST_LOGGER_SUCCESS_ENABLED("advanced.request-tracker.logs.success.enabled"), + REQUEST_LOGGER_SLOW_THRESHOLD("advanced.request-tracker.logs.slow.threshold"), + REQUEST_LOGGER_SLOW_ENABLED("advanced.request-tracker.logs.slow.enabled"), + REQUEST_LOGGER_ERROR_ENABLED("advanced.request-tracker.logs.error.enabled"), + REQUEST_LOGGER_MAX_QUERY_LENGTH("advanced.request-tracker.logs.max-query-length"), + REQUEST_LOGGER_VALUES("advanced.request-tracker.logs.show-values"), + REQUEST_LOGGER_MAX_VALUE_LENGTH("advanced.request-tracker.logs.max-value-length"), + REQUEST_LOGGER_MAX_VALUES("advanced.request-tracker.logs.max-values"), + REQUEST_LOGGER_STACK_TRACES("advanced.request-tracker.logs.show-stack-traces"), + + REQUEST_THROTTLER_CLASS("advanced.throttler.class"), + REQUEST_THROTTLER_MAX_CONCURRENT_REQUESTS("advanced.throttler.max-concurrent-requests"), + REQUEST_THROTTLER_MAX_REQUESTS_PER_SECOND("advanced.throttler.max-requests-per-second"), + REQUEST_THROTTLER_MAX_QUEUE_SIZE("advanced.throttler.max-queue-size"), + REQUEST_THROTTLER_DRAIN_INTERVAL("advanced.throttler.drain-interval"), + + METADATA_NODE_STATE_LISTENER_CLASS("advanced.node-state-listener.class"), + + METADATA_SCHEMA_CHANGE_LISTENER_CLASS("advanced.schema-change-listener.class"), + + ADDRESS_TRANSLATOR_CLASS("advanced.address-translator.class"), + + PROTOCOL_VERSION("advanced.protocol.version"), + PROTOCOL_COMPRESSION("advanced.protocol.compression"), + PROTOCOL_MAX_FRAME_LENGTH("advanced.protocol.max-frame-length"), + + REQUEST_WARN_IF_SET_KEYSPACE("advanced.request.warn-if-set-keyspace"), + REQUEST_TRACE_ATTEMPTS("advanced.request.trace.attempts"), + REQUEST_TRACE_INTERVAL("advanced.request.trace.interval"), + REQUEST_TRACE_CONSISTENCY("advanced.request.trace.consistency"), + + METRICS_SESSION_ENABLED("advanced.metrics.session.enabled"), + METRICS_NODE_ENABLED("advanced.metrics.node.enabled"), + METRICS_SESSION_CQL_REQUESTS_HIGHEST("advanced.metrics.session.cql-requests.highest-latency"), + METRICS_SESSION_CQL_REQUESTS_DIGITS("advanced.metrics.session.cql-requests.significant-digits"), + METRICS_SESSION_CQL_REQUESTS_INTERVAL("advanced.metrics.session.cql-requests.refresh-interval"), + METRICS_SESSION_THROTTLING_HIGHEST("advanced.metrics.session.throttling.delay.highest-latency"), + METRICS_SESSION_THROTTLING_DIGITS("advanced.metrics.session.throttling.delay.significant-digits"), + METRICS_SESSION_THROTTLING_INTERVAL("advanced.metrics.session.throttling.delay.refresh-interval"), + METRICS_NODE_CQL_MESSAGES_HIGHEST("advanced.metrics.node.cql-messages.highest-latency"), + METRICS_NODE_CQL_MESSAGES_DIGITS("advanced.metrics.node.cql-messages.significant-digits"), + METRICS_NODE_CQL_MESSAGES_INTERVAL("advanced.metrics.node.cql-messages.refresh-interval"), + + SOCKET_TCP_NODELAY("advanced.socket.tcp-no-delay"), + SOCKET_KEEP_ALIVE("advanced.socket.keep-alive"), + SOCKET_REUSE_ADDRESS("advanced.socket.reuse-address"), + SOCKET_LINGER_INTERVAL("advanced.socket.linger-interval"), + SOCKET_RECEIVE_BUFFER_SIZE("advanced.socket.receive-buffer-size"), + SOCKET_SEND_BUFFER_SIZE("advanced.socket.send-buffer-size"), + + HEARTBEAT_INTERVAL("advanced.heartbeat.interval"), + HEARTBEAT_TIMEOUT("advanced.heartbeat.timeout"), + + METADATA_TOPOLOGY_WINDOW("advanced.metadata.topology-event-debouncer.window"), + METADATA_TOPOLOGY_MAX_EVENTS("advanced.metadata.topology-event-debouncer.max-events"), + METADATA_SCHEMA_ENABLED("advanced.metadata.schema.enabled"), + METADATA_SCHEMA_REQUEST_TIMEOUT("advanced.metadata.schema.request-timeout"), + METADATA_SCHEMA_REQUEST_PAGE_SIZE("advanced.metadata.schema.request-page-size"), + METADATA_SCHEMA_REFRESHED_KEYSPACES("advanced.metadata.schema.refreshed-keyspaces"), + METADATA_SCHEMA_WINDOW("advanced.metadata.schema.debouncer.window"), + METADATA_SCHEMA_MAX_EVENTS("advanced.metadata.schema.debouncer.max-events"), + METADATA_TOKEN_MAP_ENABLED("advanced.metadata.token-map.enabled"), + + CONTROL_CONNECTION_TIMEOUT("advanced.control-connection.timeout"), + CONTROL_CONNECTION_AGREEMENT_INTERVAL("advanced.control-connection.schema-agreement.interval"), + CONTROL_CONNECTION_AGREEMENT_TIMEOUT("advanced.control-connection.schema-agreement.timeout"), + CONTROL_CONNECTION_AGREEMENT_WARN("advanced.control-connection.schema-agreement.warn-on-failure"), + + PREPARE_ON_ALL_NODES("advanced.prepared-statements.prepare-on-all-nodes"), + REPREPARE_ENABLED("advanced.prepared-statements.reprepare-on-up.enabled"), + REPREPARE_CHECK_SYSTEM_TABLE("advanced.prepared-statements.reprepare-on-up.check-system-table"), + REPREPARE_MAX_STATEMENTS("advanced.prepared-statements.reprepare-on-up.max-statements"), + REPREPARE_MAX_PARALLELISM("advanced.prepared-statements.reprepare-on-up.max-parallelism"), + REPREPARE_TIMEOUT("advanced.prepared-statements.reprepare-on-up.timeout"), + + NETTY_IO_SIZE("advanced.netty.io-group.size"), + NETTY_IO_SHUTDOWN_QUIET_PERIOD("advanced.netty.io-group.shutdown.quiet-period"), + NETTY_IO_SHUTDOWN_TIMEOUT("advanced.netty.io-group.shutdown.timeout"), + NETTY_IO_SHUTDOWN_UNIT("advanced.netty.io-group.shutdown.unit"), + NETTY_ADMIN_SIZE("advanced.netty.admin-group.size"), + NETTY_ADMIN_SHUTDOWN_QUIET_PERIOD("advanced.netty.admin-group.shutdown.quiet-period"), + NETTY_ADMIN_SHUTDOWN_TIMEOUT("advanced.netty.admin-group.shutdown.timeout"), + NETTY_ADMIN_SHUTDOWN_UNIT("advanced.netty.admin-group.shutdown.unit"), + + COALESCER_MAX_RUNS("advanced.coalescer.max-runs-with-no-work"), + COALESCER_INTERVAL("advanced.coalescer.reschedule-interval"), + + RESOLVE_CONTACT_POINTS("advanced.resolve-contact-points"), + + NETTY_TIMER_TICK_DURATION("advanced.netty.timer.tick-duration"), + NETTY_TIMER_TICKS_PER_WHEEL("advanced.netty.timer.ticks-per-wheel"), + + REQUEST_LOG_WARNINGS("advanced.request.log-warnings"), + ; + + private final String path; + + DefaultDriverOption(String path) { + this.path = path; + } + + @NonNull + @Override + public String getPath() { + return path; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverConfig.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverConfig.java new file mode 100644 index 00000000000..fae096123c2 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverConfig.java @@ -0,0 +1,47 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.config; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; + +/** + * The configuration of the driver. + * + *

It is composed of options, that are organized into profiles. There is a default profile that + * is always present, and additional, named profiles, that can override part of the options. + * Profiles can be used to categorize queries that use the same parameters (for example, an + * "analytics" profile vs. a "transactional" profile). + */ +public interface DriverConfig { + + /** + * Alias to get the default profile, which is stored under the name {@link + * DriverExecutionProfile#DEFAULT_NAME} and always present. + */ + @NonNull + default DriverExecutionProfile getDefaultProfile() { + return getProfile(DriverExecutionProfile.DEFAULT_NAME); + } + + /** @throws IllegalArgumentException if there is no profile with this name. */ + @NonNull + DriverExecutionProfile getProfile(@NonNull String profileName); + + /** Returns an immutable view of all named profiles (including the default profile). */ + @NonNull + Map getProfiles(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverConfigLoader.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverConfigLoader.java new file mode 100644 index 00000000000..3ff4ae5ccaf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverConfigLoader.java @@ -0,0 +1,67 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.config; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.CompletionStage; + +/** + * Manages the initialization, and optionally the periodic reloading, of the driver configuration. + */ +public interface DriverConfigLoader extends AutoCloseable { + + /** + * Loads the first configuration that will be used to initialize the driver. + * + *

If this loader {@linkplain #supportsReloading() supports reloading}, this object should be + * mutable and reflect later changes when the configuration gets reloaded. + */ + @NonNull + DriverConfig getInitialConfig(); + + /** + * Called when the driver initializes. For loaders that periodically check for configuration + * updates, this is a good time to grab an internal executor and schedule a recurring task. + */ + void onDriverInit(@NonNull DriverContext context); + + /** + * Triggers an immediate reload attempt. + * + * @return a stage that completes once the attempt is finished, with a boolean indicating whether + * the configuration changed as a result of this reload. If so, it's also guaranteed that + * internal driver components have been notified by that time; note however that some react to + * the notification asynchronously, so they may not have completely applied all resulting + * changes yet. If this loader does not support programmatic reloading — which you can + * check by calling {@link #supportsReloading()} before this method — the returned + * object will fail immediately with an {@link UnsupportedOperationException}. + */ + @NonNull + CompletionStage reload(); + + /** + * Whether this implementation supports programmatic reloading with the {@link #reload()} method. + */ + boolean supportsReloading(); + + /** + * Called when the cluster closes. This is a good time to release any external resource, for + * example cancel a scheduled reloading task. + */ + @Override + void close(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverExecutionProfile.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverExecutionProfile.java new file mode 100644 index 00000000000..57fc7f179f3 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverExecutionProfile.java @@ -0,0 +1,257 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.config; + +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; + +/** + * A profile in the driver's configuration. + * + *

It is a collection of typed options. + * + *

Getters (such as {@link #getBoolean(DriverOption)}) are self-explanatory. + * + *

{@code withXxx} methods (such as {@link #withBoolean(DriverOption, boolean)}) create a + * "derived" profile, which is an on-the-fly copy of the profile with the new value (which + * might be a new option, or overwrite an existing one). If the original configuration is reloaded, + * all derived profiles get updated as well. For best performance, such derived profiles should be + * used sparingly; it is better to have built-in profiles for common scenarios. + * + * @see DriverConfig + */ +public interface DriverExecutionProfile { + + /** + * The name of the default profile (the string {@value}). + * + *

Named profiles can't use this name. If you try to declare such a profile, a runtime error + * will be thrown. + */ + String DEFAULT_NAME = "default"; + + /** + * The name of the profile in the configuration. + * + *

Derived profiles inherit the name of their parent. + */ + @NonNull + String getName(); + + boolean isDefined(@NonNull DriverOption option); + + boolean getBoolean(@NonNull DriverOption option); + + default boolean getBoolean(@NonNull DriverOption option, boolean defaultValue) { + return isDefined(option) ? getBoolean(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withBoolean(@NonNull DriverOption option, boolean value); + + @NonNull + List getBooleanList(@NonNull DriverOption option); + + @Nullable + default List getBooleanList( + @NonNull DriverOption option, @Nullable List defaultValue) { + return isDefined(option) ? getBooleanList(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withBooleanList( + @NonNull DriverOption option, @NonNull List value); + + int getInt(@NonNull DriverOption option); + + default int getInt(@NonNull DriverOption option, int defaultValue) { + return isDefined(option) ? getInt(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withInt(@NonNull DriverOption option, int value); + + @NonNull + List getIntList(@NonNull DriverOption option); + + @Nullable + default List getIntList( + @NonNull DriverOption option, @Nullable List defaultValue) { + return isDefined(option) ? getIntList(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withIntList(@NonNull DriverOption option, @NonNull List value); + + long getLong(@NonNull DriverOption option); + + default long getLong(@NonNull DriverOption option, long defaultValue) { + return isDefined(option) ? getLong(option) : defaultValue; + } + + DriverExecutionProfile withLong(@NonNull DriverOption option, long value); + + @NonNull + List getLongList(@NonNull DriverOption option); + + @Nullable + default List getLongList(@NonNull DriverOption option, @Nullable List defaultValue) { + return isDefined(option) ? getLongList(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withLongList(@NonNull DriverOption option, @NonNull List value); + + double getDouble(@NonNull DriverOption option); + + default double getDouble(@NonNull DriverOption option, double defaultValue) { + return isDefined(option) ? getDouble(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withDouble(@NonNull DriverOption option, double value); + + @NonNull + List getDoubleList(@NonNull DriverOption option); + + @Nullable + default List getDoubleList( + @NonNull DriverOption option, @Nullable List defaultValue) { + return isDefined(option) ? getDoubleList(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withDoubleList(@NonNull DriverOption option, @NonNull List value); + + @NonNull + String getString(@NonNull DriverOption option); + + @Nullable + default String getString(@NonNull DriverOption option, @Nullable String defaultValue) { + return isDefined(option) ? getString(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withString(@NonNull DriverOption option, @NonNull String value); + + @NonNull + List getStringList(@NonNull DriverOption option); + + @Nullable + default List getStringList( + @NonNull DriverOption option, @Nullable List defaultValue) { + return isDefined(option) ? getStringList(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withStringList(@NonNull DriverOption option, @NonNull List value); + + @NonNull + Map getStringMap(@NonNull DriverOption option); + + @Nullable + default Map getStringMap( + @NonNull DriverOption option, @Nullable Map defaultValue) { + return isDefined(option) ? getStringMap(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withStringMap( + @NonNull DriverOption option, @NonNull Map value); + + /** + * @return a size in bytes. This is separate from {@link #getLong(DriverOption)}, in case + * implementations want to allow users to provide sizes in a more human-readable way, for + * example "256 MB". + */ + long getBytes(@NonNull DriverOption option); + + default long getBytes(@NonNull DriverOption option, long defaultValue) { + return isDefined(option) ? getBytes(option) : defaultValue; + } + + /** @see #getBytes(DriverOption) */ + @NonNull + DriverExecutionProfile withBytes(@NonNull DriverOption option, long value); + + /** @see #getBytes(DriverOption) */ + @NonNull + List getBytesList(DriverOption option); + + @Nullable + default List getBytesList(DriverOption option, @Nullable List defaultValue) { + return isDefined(option) ? getBytesList(option) : defaultValue; + } + + /** @see #getBytes(DriverOption) */ + @NonNull + DriverExecutionProfile withBytesList(@NonNull DriverOption option, @NonNull List value); + + @NonNull + Duration getDuration(@NonNull DriverOption option); + + @Nullable + default Duration getDuration(@NonNull DriverOption option, @Nullable Duration defaultValue) { + return isDefined(option) ? getDuration(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withDuration(@NonNull DriverOption option, @NonNull Duration value); + + @NonNull + List getDurationList(@NonNull DriverOption option); + + @Nullable + default List getDurationList( + @NonNull DriverOption option, @Nullable List defaultValue) { + return isDefined(option) ? getDurationList(option) : defaultValue; + } + + @NonNull + DriverExecutionProfile withDurationList( + @NonNull DriverOption option, @NonNull List value); + + /** Unsets an option. */ + @NonNull + DriverExecutionProfile without(@NonNull DriverOption option); + + /** + * Returns a representation of all the child options under a given option. + * + *

This is only used to compare configuration sections across profiles, so the actual + * implementation does not matter, as long as identical sections (same options with same values, + * regardless of order) compare as equal and have the same {@code hashCode()}. + */ + @NonNull + Object getComparisonKey(@NonNull DriverOption option); + + /** + * Enumerates all the entries in this profile, including those that were inherited from another + * profile. + * + *

The keys are raw strings that match {@link DriverOption#getPath()}. + * + *

The values are implementation-dependent. With the driver's default implementation, the + * possible types are {@code String}, {@code Number}, {@code Boolean}, {@code Map}, + * {@code List}, or {@code null}. + */ + @NonNull + SortedSet> entrySet(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverOption.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverOption.java new file mode 100644 index 00000000000..3213dc4b2ad --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/DriverOption.java @@ -0,0 +1,34 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.config; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Describes an option in the driver's configuration. + * + *

This is just a thin wrapper around the option's path, to make it easier to find where it is + * referenced in the code. We recommend using enums for implementations. + */ +public interface DriverOption { + + /** + * The option's path. Paths are hierarchical and each segment is separated by a dot, e.g. {@code + * metadata.schema.enabled}. + */ + @NonNull + String getPath(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/config/package-info.java b/core/src/main/java/com/datastax/oss/driver/api/core/config/package-info.java new file mode 100644 index 00000000000..6ddc5abaf62 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/config/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * The configuration of the driver. + * + *

The public API is completely agnostic to the underlying implementation (where the + * configuration is loaded from, what framework is used...). + */ +package com.datastax.oss.driver.api.core.config; diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/connection/BusyConnectionException.java b/core/src/main/java/com/datastax/oss/driver/api/core/connection/BusyConnectionException.java new file mode 100644 index 00000000000..1c725715d54 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/connection/BusyConnectionException.java @@ -0,0 +1,51 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.connection; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Indicates that a write was attempted on a connection that already handles too many simultaneous + * requests. + * + *

This might happen under heavy load. The driver will automatically try the next node in the + * query plan. Therefore the only way that the client can observe this exception is as part of a + * {@link AllNodesFailedException}. + */ +public class BusyConnectionException extends DriverException { + + public BusyConnectionException(int maxAvailableIds) { + this( + String.format( + "Connection has exceeded its maximum of %d simultaneous requests", maxAvailableIds), + null, + false); + } + + private BusyConnectionException( + String message, ExecutionInfo executionInfo, boolean writableStackTrace) { + super(message, executionInfo, null, writableStackTrace); + } + + @Override + @NonNull + public DriverException copy() { + return new BusyConnectionException(getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/connection/ClosedConnectionException.java b/core/src/main/java/com/datastax/oss/driver/api/core/connection/ClosedConnectionException.java new file mode 100644 index 00000000000..9daee547a46 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/connection/ClosedConnectionException.java @@ -0,0 +1,52 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.connection; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.DriverException; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Thrown when the connection on which a request was executing is closed due to an unrelated event. + * + *

For example, this can happen if the node is unresponsive and a heartbeat query failed, or if + * the node was forced down. + * + *

The driver will always retry these requests on the next node transparently. Therefore, the + * only way to observe this exception is as part of an {@link AllNodesFailedException}. + */ +public class ClosedConnectionException extends DriverException { + + public ClosedConnectionException(@NonNull String message) { + this(message, null, false); + } + + public ClosedConnectionException(@NonNull String message, @Nullable Throwable cause) { + this(message, cause, false); + } + + private ClosedConnectionException( + @NonNull String message, @Nullable Throwable cause, boolean writableStackTrace) { + super(message, null, cause, writableStackTrace); + } + + @Override + @NonNull + public DriverException copy() { + return new ClosedConnectionException(getMessage(), getCause(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/connection/ConnectionInitException.java b/core/src/main/java/com/datastax/oss/driver/api/core/connection/ConnectionInitException.java new file mode 100644 index 00000000000..4112bdcd6f8 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/connection/ConnectionInitException.java @@ -0,0 +1,45 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.connection; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Indicates a generic error while initializing a connection. + * + *

The only time when this is returned directly to the client (wrapped in a {@link + * AllNodesFailedException}) is at initialization. If it happens later when the driver is already + * connected, it is just logged an the connection is reattempted. + */ +public class ConnectionInitException extends DriverException { + public ConnectionInitException(@NonNull String message, @Nullable Throwable cause) { + super(message, null, cause, true); + } + + private ConnectionInitException(String message, ExecutionInfo executionInfo, Throwable cause) { + super(message, executionInfo, cause, true); + } + + @NonNull + @Override + public DriverException copy() { + return new ConnectionInitException(getMessage(), getExecutionInfo(), getCause()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/connection/FrameTooLongException.java b/core/src/main/java/com/datastax/oss/driver/api/core/connection/FrameTooLongException.java new file mode 100644 index 00000000000..e84504d089f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/connection/FrameTooLongException.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.connection; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.SocketAddress; + +/** + * Thrown when an incoming or outgoing protocol frame exceeds the limit defined by {@code + * protocol.max-frame-length} in the configuration. + * + *

This error is always rethrown directly to the client, without any retry attempt. + */ +public class FrameTooLongException extends DriverException { + + private final SocketAddress address; + + public FrameTooLongException(@NonNull SocketAddress address, @NonNull String message) { + this(address, message, null); + } + + private FrameTooLongException( + SocketAddress address, String message, ExecutionInfo executionInfo) { + super(message, executionInfo, null, false); + this.address = address; + } + + /** The address of the node that encountered the error. */ + @NonNull + public SocketAddress getAddress() { + return address; + } + + @NonNull + @Override + public DriverException copy() { + return new FrameTooLongException(address, getMessage(), getExecutionInfo()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/connection/HeartbeatException.java b/core/src/main/java/com/datastax/oss/driver/api/core/connection/HeartbeatException.java new file mode 100644 index 00000000000..183f7c5366e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/connection/HeartbeatException.java @@ -0,0 +1,60 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.connection; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.net.SocketAddress; + +/** + * Thrown when a heartbeat query fails. + * + *

Heartbeat queries are sent automatically on idle connections, to ensure that they are still + * alive. If a heartbeat query fails, the connection is closed, and all pending queries are aborted. + * The exception will be passed to {@link RetryPolicy#onRequestAborted(Request, Throwable, int)}, + * which decides what to do next (the default policy retries the query on the next node). + */ +public class HeartbeatException extends DriverException { + + private final SocketAddress address; + + public HeartbeatException( + @NonNull SocketAddress address, @Nullable String message, @Nullable Throwable cause) { + this(address, message, null, cause); + } + + public HeartbeatException( + SocketAddress address, String message, ExecutionInfo executionInfo, Throwable cause) { + super(message, executionInfo, cause, true); + this.address = address; + } + + /** The address of the node that encountered the error. */ + @NonNull + public SocketAddress getAddress() { + return address; + } + + @NonNull + @Override + public DriverException copy() { + return new HeartbeatException(address, getMessage(), getExecutionInfo(), getCause()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/connection/ReconnectionPolicy.java b/core/src/main/java/com/datastax/oss/driver/api/core/connection/ReconnectionPolicy.java new file mode 100644 index 00000000000..083e83950c6 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/connection/ReconnectionPolicy.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.connection; + +import com.datastax.oss.driver.api.core.metadata.Node; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; + +/** + * Decides how often the driver tries to re-establish lost connections. + * + *

When a reconnection starts, the driver invokes this policy to create a {@link + * ReconnectionSchedule ReconnectionSchedule} instance. That schedule's {@link + * ReconnectionSchedule#nextDelay() nextDelay()} method will get called each time the driver needs + * to program the next connection attempt. When the reconnection succeeds, the schedule is + * discarded; if the connection is lost again later, the next reconnection attempt will query the + * policy again to obtain a new schedule. + * + *

There are two types of reconnection: + * + *

    + *
  • {@linkplain #newNodeSchedule(Node) for regular node connections}: when the connection pool + * for a node does not have its configured number of connections (see {@code + * advanced.connection.pool.*.size} in the configuration), a reconnection starts for that + * pool. + *
  • {@linkplain #newControlConnectionSchedule(boolean) for the control connection}: when the + * control node goes down, a reconnection starts to find another node to replace it. This is + * also used if the configuration option {@code advanced.reconnect-on-init} is set and the + * driver has to retry the initial connection. + *
+ * + * This interface defines separate methods for those two cases, but implementations are free to + * delegate to the same method internally if the same type of schedule can be used. + */ +public interface ReconnectionPolicy extends AutoCloseable { + + /** Creates a new schedule for the given node. */ + @NonNull + ReconnectionSchedule newNodeSchedule(@NonNull Node node); + + /** + * Creates a new schedule for the control connection. + * + * @param isInitialConnection whether this schedule is generated for the driver's initial attempt + * to connect to the cluster. + *
    + *
  • {@code true} means that the configuration option {@code advanced.reconnect-on-init} + * is set, the driver failed to reach any contact point, and it is now scheduling + * reattempts. + *
  • {@code false} means that the driver was already initialized, lost connection to the + * control node, and is now scheduling attempts to connect to another node. + *
+ */ + @NonNull + ReconnectionSchedule newControlConnectionSchedule(boolean isInitialConnection); + + /** Called when the cluster that this policy is associated with closes. */ + @Override + void close(); + + /** + * The reconnection schedule from the time a connection is lost, to the time all connections to + * this node have been restored. + */ + interface ReconnectionSchedule { + /** How long to wait before the next reconnection attempt. */ + @NonNull + Duration nextDelay(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/connection/package-info.java b/core/src/main/java/com/datastax/oss/driver/api/core/connection/package-info.java new file mode 100644 index 00000000000..ade4f228669 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/connection/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Types related to a connection to a Cassandra node. + * + *

The driver generally connects to multiple nodes, and may keep multiple connections to each + * node. + */ +package com.datastax.oss.driver.api.core.connection; diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/context/DriverContext.java b/core/src/main/java/com/datastax/oss/driver/api/core/context/DriverContext.java new file mode 100644 index 00000000000..b4efb691494 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/context/DriverContext.java @@ -0,0 +1,151 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.context; + +import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; +import com.datastax.oss.driver.api.core.auth.AuthProvider; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; +import com.datastax.oss.driver.api.core.time.TimestampGenerator; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import java.util.Optional; + +/** Holds common components that are shared throughout a driver instance. */ +public interface DriverContext extends AttachmentPoint { + + /** + * This is the same as {@link Session#getName()}, it's exposed here for components that only have + * a reference to the context. + */ + @NonNull + String getSessionName(); + + /** @return The driver's configuration; never {@code null}. */ + @NonNull + DriverConfig getConfig(); + + /** @return The driver's configuration loader; never {@code null}. */ + @NonNull + DriverConfigLoader getConfigLoader(); + + /** + * @return The driver's load balancing policies, keyed by profile name; the returned map is + * guaranteed to never be {@code null} and to always contain an entry for the {@value + * DriverExecutionProfile#DEFAULT_NAME} profile. + */ + @NonNull + Map getLoadBalancingPolicies(); + + /** + * @param profileName the profile name; never {@code null}. + * @return The driver's load balancing policy for the given profile; never {@code null}. + */ + @NonNull + default LoadBalancingPolicy getLoadBalancingPolicy(@NonNull String profileName) { + LoadBalancingPolicy policy = getLoadBalancingPolicies().get(profileName); + // Protect against a non-existent name + return (policy != null) + ? policy + : getLoadBalancingPolicies().get(DriverExecutionProfile.DEFAULT_NAME); + } + + /** + * @return The driver's retry policies, keyed by profile name; the returned map is guaranteed to + * never be {@code null} and to always contain an entry for the {@value + * DriverExecutionProfile#DEFAULT_NAME} profile. + */ + @NonNull + Map getRetryPolicies(); + + /** + * @param profileName the profile name; never {@code null}. + * @return The driver's retry policy for the given profile; never {@code null}. + */ + @NonNull + default RetryPolicy getRetryPolicy(@NonNull String profileName) { + RetryPolicy policy = getRetryPolicies().get(profileName); + return (policy != null) ? policy : getRetryPolicies().get(DriverExecutionProfile.DEFAULT_NAME); + } + + /** + * @return The driver's speculative execution policies, keyed by profile name; the returned map is + * guaranteed to never be {@code null} and to always contain an entry for the {@value + * DriverExecutionProfile#DEFAULT_NAME} profile. + */ + @NonNull + Map getSpeculativeExecutionPolicies(); + + /** + * @param profileName the profile name; never {@code null}. + * @return The driver's speculative execution policy for the given profile; never {@code null}. + */ + @NonNull + default SpeculativeExecutionPolicy getSpeculativeExecutionPolicy(@NonNull String profileName) { + SpeculativeExecutionPolicy policy = getSpeculativeExecutionPolicies().get(profileName); + return (policy != null) + ? policy + : getSpeculativeExecutionPolicies().get(DriverExecutionProfile.DEFAULT_NAME); + } + + /** @return The driver's timestamp generator; never {@code null}. */ + @NonNull + TimestampGenerator getTimestampGenerator(); + + /** @return The driver's reconnection policy; never {@code null}. */ + @NonNull + ReconnectionPolicy getReconnectionPolicy(); + + /** @return The driver's address translator; never {@code null}. */ + @NonNull + AddressTranslator getAddressTranslator(); + + /** @return The authentication provider, if authentication was configured. */ + @NonNull + Optional getAuthProvider(); + + /** @return The SSL engine factory, if SSL was configured. */ + @NonNull + Optional getSslEngineFactory(); + + /** @return The driver's request tracker; never {@code null}. */ + @NonNull + RequestTracker getRequestTracker(); + + /** @return The driver's request throttler; never {@code null}. */ + @NonNull + RequestThrottler getRequestThrottler(); + + /** @return The driver's node state listener; never {@code null}. */ + @NonNull + NodeStateListener getNodeStateListener(); + + /** @return The driver's schema change listener; never {@code null}. */ + @NonNull + SchemaChangeListener getSchemaChangeListener(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/AsyncResultSet.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/AsyncResultSet.java new file mode 100644 index 00000000000..a21c0ee8cd6 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/AsyncResultSet.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.AsyncPagingIterable; +import com.datastax.oss.driver.api.core.CqlSession; + +/** + * The result of an asynchronous CQL query. + * + * @see CqlSession#executeAsync(Statement) + * @see CqlSession#executeAsync(String) + */ +public interface AsyncResultSet extends AsyncPagingIterable { + + // overridden to amend the javadocs: + /** + * {@inheritDoc} + * + *

This is equivalent to calling: + * + *

+   *   this.iterator().next().getBoolean("[applied]")
+   * 
+ * + * Except that this method peeks at the next row without consuming it. + */ + @Override + boolean wasApplied(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchStatement.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchStatement.java new file mode 100644 index 00000000000..95c09653b1a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchStatement.java @@ -0,0 +1,253 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.internal.core.cql.DefaultBatchStatement; +import com.datastax.oss.driver.internal.core.time.ServerSideTimestampGenerator; +import com.datastax.oss.driver.internal.core.util.Sizes; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.PrimitiveSizes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +/** + * A statement that groups a number of other statements, so that they can be executed as a batch + * (i.e. sent together as a single protocol frame). + * + *

The default implementation returned by the driver is immutable and thread-safe. + * All mutating methods return a new instance. See also the static factory methods and builders in + * this interface. + */ +public interface BatchStatement extends Statement, Iterable> { + + /** Creates an instance of the default implementation for the given batch type. */ + @NonNull + static BatchStatement newInstance(@NonNull BatchType batchType) { + return new DefaultBatchStatement( + batchType, + new ArrayList<>(), + null, + null, + null, + null, + null, + null, + Collections.emptyMap(), + false, + false, + Long.MIN_VALUE, + null, + Integer.MIN_VALUE, + null, + null, + null, + null); + } + + /** + * Creates an instance of the default implementation for the given batch type, containing the + * given statements. + */ + @NonNull + static BatchStatement newInstance( + @NonNull BatchType batchType, @NonNull Iterable> statements) { + return new DefaultBatchStatement( + batchType, + ImmutableList.copyOf(statements), + null, + null, + null, + null, + null, + null, + Collections.emptyMap(), + false, + false, + Long.MIN_VALUE, + null, + Integer.MIN_VALUE, + null, + null, + null, + null); + } + + /** + * Creates an instance of the default implementation for the given batch type, containing the + * given statements. + */ + @NonNull + static BatchStatement newInstance( + @NonNull BatchType batchType, @NonNull BatchableStatement... statements) { + return new DefaultBatchStatement( + batchType, + ImmutableList.copyOf(statements), + null, + null, + null, + null, + null, + null, + Collections.emptyMap(), + false, + false, + Long.MIN_VALUE, + null, + Integer.MIN_VALUE, + null, + null, + null, + null); + } + + /** Returns a builder to create an instance of the default implementation. */ + @NonNull + static BatchStatementBuilder builder(@NonNull BatchType batchType) { + return new BatchStatementBuilder(batchType); + } + + /** + * Returns a builder to create an instance of the default implementation, copying the fields of + * the given statement. + */ + @NonNull + static BatchStatementBuilder builder(@NonNull BatchStatement template) { + return new BatchStatementBuilder(template); + } + + @NonNull + BatchType getBatchType(); + + /** + * Sets the batch type. + * + *

The driver's built-in implementation is immutable, and returns a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + */ + @NonNull + BatchStatement setBatchType(@NonNull BatchType newBatchType); + + /** + * Sets the CQL keyspace to associate with this batch. + * + *

If the keyspace is not set explicitly with this method, it will be inferred from the first + * simple statement in the batch that has a keyspace set (or will be null if no such statement + * exists). + * + *

This feature is only available with {@link DefaultProtocolVersion#V5 native protocol v5} or + * higher. Specifying a per-request keyspace with lower protocol versions will cause a runtime + * error. + * + * @see Request#getKeyspace() + */ + @NonNull + BatchStatement setKeyspace(@Nullable CqlIdentifier newKeyspace); + + /** + * Shortcut for {@link #setKeyspace(CqlIdentifier) + * setKeyspace(CqlIdentifier.fromCql(newKeyspaceName))}. + */ + @NonNull + default BatchStatement setKeyspace(@NonNull String newKeyspaceName) { + return setKeyspace(CqlIdentifier.fromCql(newKeyspaceName)); + } + + /** + * Adds a new statement to the batch. + * + *

Note that, due to protocol limitations, simple statements with named values are currently + * not supported. + * + *

The driver's built-in implementation is immutable, and returns a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + */ + @NonNull + BatchStatement add(@NonNull BatchableStatement statement); + + /** + * Adds new statements to the batch. + * + *

Note that, due to protocol limitations, simple statements with named values are currently + * not supported. + * + *

The driver's built-in implementation is immutable, and returns a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + */ + @NonNull + BatchStatement addAll(@NonNull Iterable> statements); + + /** @see #addAll(Iterable) */ + @NonNull + default BatchStatement addAll(@NonNull BatchableStatement... statements) { + return addAll(Arrays.asList(statements)); + } + + /** @return The number of child statements in this batch. */ + int size(); + + /** + * Clears the batch, removing all the statements added so far. + * + *

The driver's built-in implementation is immutable, and returns a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + */ + @NonNull + BatchStatement clear(); + + @Override + default int computeSizeInBytes(@NonNull DriverContext context) { + int size = Sizes.minimumStatementSize(this, context); + + // BatchStatement's additional elements to take into account are: + // - batch type + // - inner statements (simple or bound) + // - per-query keyspace + // - timestamp + + // batch type + size += PrimitiveSizes.BYTE; + + // inner statements + size += PrimitiveSizes.SHORT; // number of statements + + for (BatchableStatement batchableStatement : this) { + size += + Sizes.sizeOfInnerBatchStatementInBytes( + batchableStatement, context.getProtocolVersion(), context.getCodecRegistry()); + } + + // per-query keyspace + if (getKeyspace() != null) { + size += PrimitiveSizes.sizeOfString(getKeyspace().asInternal()); + } + + // timestamp + if (!(context.getTimestampGenerator() instanceof ServerSideTimestampGenerator) + || getQueryTimestamp() != Long.MIN_VALUE) { + + size += PrimitiveSizes.LONG; + } + + return size; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchStatementBuilder.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchStatementBuilder.java new file mode 100644 index 00000000000..de3283b4a36 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchStatementBuilder.java @@ -0,0 +1,156 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.core.cql.DefaultBatchStatement; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterables; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Arrays; +import net.jcip.annotations.NotThreadSafe; + +@NotThreadSafe +public class BatchStatementBuilder extends StatementBuilder { + + @NonNull private BatchType batchType; + @Nullable private CqlIdentifier keyspace; + @NonNull private ImmutableList.Builder> statementsBuilder; + private int statementsCount; + + public BatchStatementBuilder(@NonNull BatchType batchType) { + this.batchType = batchType; + this.statementsBuilder = ImmutableList.builder(); + } + + public BatchStatementBuilder(@NonNull BatchStatement template) { + super(template); + this.batchType = template.getBatchType(); + this.statementsBuilder = ImmutableList.>builder().addAll(template); + this.statementsCount = template.size(); + } + + /** + * Sets the CQL keyspace to execute this batch in. + * + * @return this builder; never {@code null}. + * @see BatchStatement#getKeyspace() + */ + @NonNull + public BatchStatementBuilder setKeyspace(@NonNull CqlIdentifier keyspace) { + this.keyspace = keyspace; + return this; + } + + /** + * Sets the CQL keyspace to execute this batch in. Shortcut for {@link #setKeyspace(CqlIdentifier) + * setKeyspace(CqlIdentifier.fromCql(keyspaceName))}. + * + * @return this builder; never {@code null}. + */ + @NonNull + public BatchStatementBuilder setKeyspace(@NonNull String keyspaceName) { + return setKeyspace(CqlIdentifier.fromCql(keyspaceName)); + } + + /** + * Adds a new statement to the batch. + * + * @return this builder; never {@code null}. + * @see BatchStatement#add(BatchableStatement) + */ + @NonNull + public BatchStatementBuilder addStatement(@NonNull BatchableStatement statement) { + if (statementsCount >= 0xFFFF) { + throw new IllegalStateException( + "Batch statement cannot contain more than " + 0xFFFF + " statements."); + } + statementsCount += 1; + statementsBuilder.add(statement); + return this; + } + + /** + * Adds new statements to the batch. + * + * @return this builder; never {@code null}. + * @see BatchStatement#addAll(Iterable) + */ + @NonNull + public BatchStatementBuilder addStatements(@NonNull Iterable> statements) { + int delta = Iterables.size(statements); + if (statementsCount + delta > 0xFFFF) { + throw new IllegalStateException( + "Batch statement cannot contain more than " + 0xFFFF + " statements."); + } + statementsCount += delta; + statementsBuilder.addAll(statements); + return this; + } + + /** + * Adds new statements to the batch. + * + * @return this builder; never {@code null}. + * @see BatchStatement#addAll(BatchableStatement[]) + */ + @NonNull + public BatchStatementBuilder addStatements(@NonNull BatchableStatement... statements) { + return addStatements(Arrays.asList(statements)); + } + + /** + * Clears all the statements in this batch. + * + * @return this builder; never {@code null}. + */ + @NonNull + public BatchStatementBuilder clearStatements() { + statementsBuilder = ImmutableList.builder(); + statementsCount = 0; + return this; + } + + /** @return a newly-allocated {@linkplain BatchStatement batch}; never {@code null}.. */ + @Override + @NonNull + public BatchStatement build() { + return new DefaultBatchStatement( + batchType, + statementsBuilder.build(), + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + buildCustomPayload(), + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + public int getStatementsCount() { + return this.statementsCount; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchType.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchType.java new file mode 100644 index 00000000000..f81d6c326bf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchType.java @@ -0,0 +1,32 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +/** + * The type of a batch. + * + *

The only reason to model this as an interface (as opposed to an enum type) is to accommodate + * for custom protocol extensions. If you're connecting to a standard Apache Cassandra cluster, all + * {@code BatchType}s are {@link DefaultBatchType} instances. + */ +public interface BatchType { + + /** The numerical value that the batch type is encoded to. */ + byte getProtocolCode(); + + // Implementation note: we don't have a "BatchTypeRegistry" because we never decode batch types. + // This can be added later if needed (see ConsistencyLevelRegistry for an example). +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchableStatement.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchableStatement.java new file mode 100644 index 00000000000..5fb50fc5348 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BatchableStatement.java @@ -0,0 +1,24 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +/** + * A statement that can be added to a CQL batch. + * + * @param the "self type" used for covariant returns in subtypes. + */ +public interface BatchableStatement> + extends Statement {} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/Bindable.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/Bindable.java new file mode 100644 index 00000000000..dc9577ae23e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/Bindable.java @@ -0,0 +1,102 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.data.AccessibleByName; +import com.datastax.oss.driver.api.core.data.GettableById; +import com.datastax.oss.driver.api.core.data.GettableByName; +import com.datastax.oss.driver.api.core.data.SettableById; +import com.datastax.oss.driver.api.core.data.SettableByName; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import edu.umd.cs.findbugs.annotations.CheckReturnValue; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** A data container with the ability to unset values. */ +public interface Bindable> + extends GettableById, GettableByName, SettableById, SettableByName { + /** + * Whether the {@code i}th value has been set. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @SuppressWarnings("ReferenceEquality") + default boolean isSet(int i) { + return getBytesUnsafe(i) != ProtocolConstants.UNSET_VALUE; + } + + /** + * Whether the value for the first occurrence of {@code id} has been set. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IndexOutOfBoundsException if the id is invalid. + */ + @SuppressWarnings("ReferenceEquality") + default boolean isSet(@NonNull CqlIdentifier id) { + return getBytesUnsafe(id) != ProtocolConstants.UNSET_VALUE; + } + + /** + * Whether the value for the first occurrence of {@code name} has been set. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IndexOutOfBoundsException if the name is invalid. + */ + @SuppressWarnings("ReferenceEquality") + default boolean isSet(@NonNull String name) { + return getBytesUnsafe(name) != ProtocolConstants.UNSET_VALUE; + } + + /** + * Unsets the {@code i}th value. This will leave the statement in the same state as if no setter + * was ever called for this value. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT unset(int i) { + return setBytesUnsafe(i, ProtocolConstants.UNSET_VALUE); + } + + /** + * Unsets the value for the first occurrence of {@code id}. This will leave the statement in the + * same state as if no setter was ever called for this value. + * + * @throws IndexOutOfBoundsException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT unset(@NonNull CqlIdentifier id) { + return setBytesUnsafe(id, ProtocolConstants.UNSET_VALUE); + } + + /** + * Unsets the value for the first occurrence of {@code name}. This will leave the statement in the + * same state as if no setter was ever called for this value. + * + * @throws IndexOutOfBoundsException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT unset(@NonNull String name) { + return setBytesUnsafe(name, ProtocolConstants.UNSET_VALUE); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/BoundStatement.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BoundStatement.java new file mode 100644 index 00000000000..c8d17189721 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BoundStatement.java @@ -0,0 +1,95 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.internal.core.time.ServerSideTimestampGenerator; +import com.datastax.oss.driver.internal.core.util.Sizes; +import com.datastax.oss.protocol.internal.PrimitiveSizes; +import com.datastax.oss.protocol.internal.request.query.Values; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * A prepared statement in its executable form, with values bound to the variables. + * + *

The default implementation returned by the driver is immutable and thread-safe. + * All mutating methods return a new instance. + */ +public interface BoundStatement + extends BatchableStatement, Bindable { + + /** The prepared statement that was used to create this statement. */ + @NonNull + PreparedStatement getPreparedStatement(); + + /** The values to bind, in their serialized form. */ + @NonNull + List getValues(); + + /** + * Always returns {@code null} (bound statements can't have a per-request keyspace, they always + * inherit the one of the statement that was initially prepared). + */ + @Override + @Nullable + default CqlIdentifier getKeyspace() { + return null; + } + + @Override + default int computeSizeInBytes(@NonNull DriverContext context) { + int size = Sizes.minimumStatementSize(this, context); + + // BoundStatement's additional elements to take into account are: + // - prepared ID + // - result metadata ID + // - parameters + // - page size + // - paging state + // - timestamp + + // prepared ID + size += PrimitiveSizes.sizeOfShortBytes(getPreparedStatement().getId().array()); + + // result metadata ID + if (getPreparedStatement().getResultMetadataId() != null) { + size += PrimitiveSizes.sizeOfShortBytes(getPreparedStatement().getResultMetadataId().array()); + } + + // parameters (always sent as positional values for bound statements) + size += Values.sizeOfPositionalValues(getValues()); + + // page size + size += PrimitiveSizes.INT; + + // paging state + if (getPagingState() != null) { + size += PrimitiveSizes.sizeOfBytes(getPagingState()); + } + + // timestamp + if (!(context.getTimestampGenerator() instanceof ServerSideTimestampGenerator) + || getQueryTimestamp() != Long.MIN_VALUE) { + size += PrimitiveSizes.LONG; + } + + return size; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/BoundStatementBuilder.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BoundStatementBuilder.java new file mode 100644 index 00000000000..579dd8e399b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/BoundStatementBuilder.java @@ -0,0 +1,174 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.cql.DefaultBoundStatement; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Map; +import net.jcip.annotations.NotThreadSafe; + +@NotThreadSafe +public class BoundStatementBuilder extends StatementBuilder + implements Bindable { + + @NonNull private final PreparedStatement preparedStatement; + @NonNull private final ColumnDefinitions variableDefinitions; + @NonNull private final ByteBuffer[] values; + @NonNull private final CodecRegistry codecRegistry; + @NonNull private final ProtocolVersion protocolVersion; + + public BoundStatementBuilder( + @NonNull PreparedStatement preparedStatement, + @NonNull ColumnDefinitions variableDefinitions, + @NonNull ByteBuffer[] values, + @Nullable String executionProfileName, + @Nullable DriverExecutionProfile executionProfile, + @Nullable CqlIdentifier routingKeyspace, + @Nullable ByteBuffer routingKey, + @Nullable Token routingToken, + @NonNull Map customPayload, + @Nullable Boolean idempotent, + boolean tracing, + long timestamp, + @Nullable ByteBuffer pagingState, + int pageSize, + @Nullable ConsistencyLevel consistencyLevel, + @Nullable ConsistencyLevel serialConsistencyLevel, + @Nullable Duration timeout, + @NonNull CodecRegistry codecRegistry, + @NonNull ProtocolVersion protocolVersion) { + this.preparedStatement = preparedStatement; + this.variableDefinitions = variableDefinitions; + this.values = values; + this.executionProfileName = executionProfileName; + this.executionProfile = executionProfile; + this.routingKeyspace = routingKeyspace; + this.routingKey = routingKey; + this.routingToken = routingToken; + for (Map.Entry entry : customPayload.entrySet()) { + this.addCustomPayload(entry.getKey(), entry.getValue()); + } + this.idempotent = idempotent; + this.tracing = tracing; + this.timestamp = timestamp; + this.pagingState = pagingState; + this.pageSize = pageSize; + this.consistencyLevel = consistencyLevel; + this.serialConsistencyLevel = serialConsistencyLevel; + this.timeout = timeout; + this.codecRegistry = codecRegistry; + this.protocolVersion = protocolVersion; + } + + public BoundStatementBuilder(@NonNull BoundStatement template) { + super(template); + this.preparedStatement = template.getPreparedStatement(); + this.variableDefinitions = template.getPreparedStatement().getVariableDefinitions(); + this.values = template.getValues().toArray(new ByteBuffer[this.variableDefinitions.size()]); + this.codecRegistry = template.codecRegistry(); + this.protocolVersion = template.protocolVersion(); + this.node = template.getNode(); + } + + @Override + public int firstIndexOf(@NonNull CqlIdentifier id) { + int indexOf = variableDefinitions.firstIndexOf(id); + if (indexOf == -1) { + throw new IllegalArgumentException(id + " is not a variable in this bound statement"); + } + return indexOf; + } + + @Override + public int firstIndexOf(@NonNull String name) { + int indexOf = variableDefinitions.firstIndexOf(name); + if (indexOf == -1) { + throw new IllegalArgumentException(name + " is not a variable in this bound statement"); + } + return indexOf; + } + + @NonNull + @Override + public BoundStatementBuilder setBytesUnsafe(int i, ByteBuffer v) { + values[i] = v; + return this; + } + + @Override + public ByteBuffer getBytesUnsafe(int i) { + return values[i]; + } + + @Override + public int size() { + return values.length; + } + + @NonNull + @Override + public DataType getType(int i) { + return variableDefinitions.get(i).getType(); + } + + @NonNull + @Override + public CodecRegistry codecRegistry() { + return codecRegistry; + } + + @NonNull + @Override + public ProtocolVersion protocolVersion() { + return protocolVersion; + } + + @NonNull + @Override + public BoundStatement build() { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + buildCustomPayload(), + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/ColumnDefinition.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/ColumnDefinition.java new file mode 100644 index 00000000000..5bdee0410ad --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/ColumnDefinition.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.detach.Detachable; +import com.datastax.oss.driver.api.core.type.DataType; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Metadata about a CQL column. + * + *

The default implementation returned by the driver is immutable and serializable. If you write + * your own implementation, it should at least be thread-safe; serializability is not mandatory, but + * recommended for use with some 3rd-party tools like Apache Spark ™. + */ +public interface ColumnDefinition extends Detachable { + + @NonNull + CqlIdentifier getKeyspace(); + + @NonNull + CqlIdentifier getTable(); + + @NonNull + CqlIdentifier getName(); + + @NonNull + DataType getType(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/ColumnDefinitions.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/ColumnDefinitions.java new file mode 100644 index 00000000000..15b206c0a6f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/ColumnDefinitions.java @@ -0,0 +1,118 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.data.AccessibleByName; +import com.datastax.oss.driver.api.core.detach.Detachable; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Metadata about a set of CQL columns. + * + *

The default implementation returned by the driver is immutable and serializable. If you write + * your own implementation, it should at least be thread-safe; serializability is not mandatory, but + * recommended for use with some 3rd-party tools like Apache Spark ™. + */ +public interface ColumnDefinitions extends Iterable, Detachable { + + /** @return the number of definitions contained in this metadata. */ + int size(); + + /** + * @param i the index to check. + * @throws IndexOutOfBoundsException if the index is invalid. + * @return the {@code i}th {@link ColumnDefinition} in this metadata. + */ + @NonNull + ColumnDefinition get(int i); + + /** + * Get a definition by name. + * + *

This is the equivalent of: + * + *

+   *   get(firstIndexOf(name))
+   * 
+ * + * @throws IllegalArgumentException if the name does not exist (in other words, if {@code + * !contains(name))}). + * @see #contains(String) + * @see #firstIndexOf(String) + */ + @NonNull + default ColumnDefinition get(@NonNull String name) { + if (!contains(name)) { + throw new IllegalArgumentException("No definition named " + name); + } else { + return get(firstIndexOf(name)); + } + } + + /** + * Get a definition by name. + * + *

This is the equivalent of: + * + *

+   *   get(firstIndexOf(name))
+   * 
+ * + * @throws IllegalArgumentException if the name does not exist (in other words, if {@code + * !contains(name))}). + * @see #contains(CqlIdentifier) + * @see #firstIndexOf(CqlIdentifier) + */ + @NonNull + default ColumnDefinition get(@NonNull CqlIdentifier name) { + if (!contains(name)) { + throw new IllegalArgumentException("No definition named " + name); + } else { + return get(firstIndexOf(name)); + } + } + + /** + * Whether there is a definition using the given name. + * + *

Because raw strings are ambiguous with regard to case-sensitivity, the argument will be + * interpreted according to the rules described in {@link AccessibleByName}. + */ + boolean contains(@NonNull String name); + + /** Whether there is a definition using the given CQL identifier. */ + boolean contains(@NonNull CqlIdentifier id); + + /** + * Returns the index of the first column that uses the given name. + * + *

Because raw strings are ambiguous with regard to case-sensitivity, the argument will be + * interpreted according to the rules described in {@link AccessibleByName}. + * + *

Also, note that if multiple columns use the same name, there is no way to find the index for + * the next occurrences. One way to avoid this is to use aliases in your CQL queries. + */ + int firstIndexOf(@NonNull String name); + + /** + * Returns the index of the first column that uses the given identifier. + * + *

Note that if multiple columns use the same identifier, there is no way to find the index for + * the next occurrences. One way to avoid this is to use aliases in your CQL queries. + */ + int firstIndexOf(@NonNull CqlIdentifier id); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/DefaultBatchType.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/DefaultBatchType.java new file mode 100644 index 00000000000..f941d48906d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/DefaultBatchType.java @@ -0,0 +1,52 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.protocol.internal.ProtocolConstants; + +/** A default batch type supported by the driver out of the box. */ +public enum DefaultBatchType implements BatchType { + /** + * A logged batch: Cassandra will first write the batch to its distributed batch log to ensure the + * atomicity of the batch (atomicity meaning that if any statement in the batch succeeds, all will + * eventually succeed). + */ + LOGGED(ProtocolConstants.BatchType.LOGGED), + + /** + * A batch that doesn't use Cassandra's distributed batch log. Such batch are not guaranteed to be + * atomic. + */ + UNLOGGED(ProtocolConstants.BatchType.UNLOGGED), + + /** + * A counter batch. Note that such batch is the only type that can contain counter operations and + * it can only contain these. + */ + COUNTER(ProtocolConstants.BatchType.COUNTER), + ; + + private final byte code; + + DefaultBatchType(byte code) { + this.code = code; + } + + @Override + public byte getProtocolCode() { + return code; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/ExecutionInfo.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/ExecutionInfo.java new file mode 100644 index 00000000000..5187966b720 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/ExecutionInfo.java @@ -0,0 +1,210 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.RequestThrottlingException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryDecision; +import com.datastax.oss.driver.api.core.servererrors.CoordinatorException; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletionStage; + +/** + * Information about the execution of a query. + * + *

This can be obtained either from a result set for a successful query, or from a driver + * exception for a failed query. + * + * @see ResultSet#getExecutionInfo() + * @see DriverException#getExecutionInfo() + */ +public interface ExecutionInfo { + + /** The statement that was executed. */ + @NonNull + Statement getStatement(); + + /** + * The node that acted as a coordinator for the query. + * + *

For successful queries, this is never {@code null}. It is the node that sent the response + * from which the result was decoded. + * + *

For failed queries, this can either be {@code null} if the error occurred before any node + * could be contacted (for example a {@link RequestThrottlingException}), or present if a node was + * successfully contacted, but replied with an error response (any subclass of {@link + * CoordinatorException}). + */ + @Nullable + Node getCoordinator(); + + /** + * The number of speculative executions that were started for this query. + * + *

This does not include the initial, normal execution of the query. Therefore, if speculative + * executions are disabled, this will always be 0. If they are enabled and one speculative + * execution was triggered in addition to the initial execution, this will be 1, etc. + * + * @see SpeculativeExecutionPolicy + */ + int getSpeculativeExecutionCount(); + + /** + * The index of the execution that completed this query. + * + *

0 represents the initial, normal execution of the query, 1 the first speculative execution, + * etc. If this execution info is attached to an error, this might not be applicable, and will + * return -1. + * + * @see SpeculativeExecutionPolicy + */ + int getSuccessfulExecutionIndex(); + + /** + * The errors encountered on previous coordinators, if any. + * + *

The list is in chronological order, based on the time that the driver processed the error + * responses. If speculative executions are enabled, they run concurrently so their errors will be + * interleaved. A node can appear multiple times (if the retry policy decided to retry on the same + * node). + */ + @NonNull + List> getErrors(); + + /** + * The paging state of the query. + * + *

This represents the next page to be fetched if this query has multiple page of results. It + * can be saved and reused later on the same statement. + * + * @return the paging state, or {@code null} if there is no next page. + */ + @Nullable + ByteBuffer getPagingState(); + + /** + * The server-side warnings for this query, if any (otherwise the list will be empty). + * + *

This feature is only available with {@link DefaultProtocolVersion#V4} or above; with lower + * versions, this list will always be empty. + */ + @NonNull + List getWarnings(); + + /** + * The custom payload sent back by the server with the response, if any (otherwise the map will be + * empty). + * + *

This method returns a read-only view of the original map, but its values remain inherently + * mutable. If multiple clients will read these values, care should be taken not to corrupt the + * data (in particular, preserve the indices by calling {@link ByteBuffer#duplicate()}). + * + *

This feature is only available with {@link DefaultProtocolVersion#V4} or above; with lower + * versions, this map will always be empty. + */ + @NonNull + Map getIncomingPayload(); + + /** + * Whether the cluster reached schema agreement after the execution of this query. + * + *

After a successful schema-altering query (ex: creating a table), the driver will check if + * the cluster's nodes agree on the new schema version. If not, it will keep retrying a few times + * (the retry delay and timeout are set through the configuration). + * + *

If this method returns {@code false}, clients can call {@link + * Session#checkSchemaAgreement()} later to perform the check manually. + * + *

Schema agreement is only checked for schema-altering queries. For other query types, this + * method will always return {@code true}. + * + * @see DefaultDriverOption#CONTROL_CONNECTION_AGREEMENT_INTERVAL + * @see DefaultDriverOption#CONTROL_CONNECTION_AGREEMENT_TIMEOUT + */ + boolean isSchemaInAgreement(); + + /** + * The tracing identifier if tracing was {@link Statement#isTracing() enabled} for this query, + * otherwise {@code null}. + */ + @Nullable + UUID getTracingId(); + + /** + * Fetches the query trace asynchronously, if tracing was enabled for this query. + * + *

Note that each call to this method triggers a new fetch, even if the previous call was + * successful (this allows fetching the trace again if the list of {@link QueryTrace#getEvents() + * events} was incomplete). + * + *

This method will return a failed future if tracing was disabled for the query (that is, if + * {@link #getTracingId()} is null). + */ + @NonNull + CompletionStage getQueryTraceAsync(); + + /** + * Convenience method to call {@link #getQueryTraceAsync()} and block for the result. + * + *

This must not be called on a driver thread. + * + * @throws IllegalStateException if {@link #getTracingId()} is null. + */ + @NonNull + default QueryTrace getQueryTrace() { + BlockingOperation.checkNotDriverThread(); + return CompletableFutures.getUninterruptibly(getQueryTraceAsync()); + } + + /** + * The size of the binary response in bytes. + * + *

This is the size of the protocol-level frame (including the frame header) before it was + * decoded by the driver, but after decompression (if compression is enabled). + * + *

If the information is not available (for example if this execution info comes from an {@link + * RetryDecision#IGNORE IGNORE} decision of the retry policy), this method returns -1. + * + * @see #getCompressedResponseSizeInBytes() + */ + int getResponseSizeInBytes(); + + /** + * The size of the compressed binary response in bytes. + * + *

This is the size of the protocol-level frame (including the frame header) as it came in the + * TCP response, before decompression and decoding by the driver. + * + *

If compression is disabled, or if the information is not available (for example if this + * execution info comes from an {@link RetryDecision#IGNORE IGNORE} decision of the retry policy), + * this method returns -1. + * + * @see #getResponseSizeInBytes() + */ + int getCompressedResponseSizeInBytes(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/PrepareRequest.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/PrepareRequest.java new file mode 100644 index 00000000000..3e7308ccd4f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/PrepareRequest.java @@ -0,0 +1,171 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +/** + * A request to prepare a CQL query. + * + *

Driver clients should rarely have to deal directly with this type, it's used internally by + * {@link Session}'s prepare methods. However a {@link RetryPolicy} implementation might use it if + * it needs a custom behavior for prepare requests. + * + *

A client may also provide their own implementation of this interface to customize which + * attributes are propagated when preparing a simple statement; see {@link + * CqlSession#prepare(SimpleStatement)} for more explanations. + */ +public interface PrepareRequest extends Request { + + /** + * The type returned when a CQL statement is prepared synchronously. + * + *

Most users won't use this explicitly. It is needed for the generic execute method ({@link + * Session#execute(Request, GenericType)}), but CQL statements will generally be prepared with one + * of the driver's built-in helper methods (such as {@link CqlSession#prepare(SimpleStatement)}). + */ + GenericType SYNC = GenericType.of(PreparedStatement.class); + + /** + * The type returned when a CQL statement is prepared asynchronously. + * + *

Most users won't use this explicitly. It is needed for the generic execute method ({@link + * Session#execute(Request, GenericType)}), but CQL statements will generally be prepared with one + * of the driver's built-in helper methods (such as {@link + * CqlSession#prepareAsync(SimpleStatement)}. + */ + GenericType> ASYNC = + new GenericType>() {}; + + /** The CQL query to prepare. */ + @NonNull + String getQuery(); + + /** + * {@inheritDoc} + * + *

Note that this refers to the prepare query itself, not to the bound statements that will be + * created from the prepared statement (see {@link #areBoundStatementsIdempotent()}). + */ + @NonNull + @Override + default Boolean isIdempotent() { + // Retrying to prepare is always safe + return true; + } + + /** + * The name of the execution profile to use for the bound statements that will be created from the + * prepared statement. + * + *

Note that this will be ignored if {@link #getExecutionProfileForBoundStatements()} returns a + * non-null value. + */ + @Nullable + String getExecutionProfileNameForBoundStatements(); + + /** + * The execution profile to use for the bound statements that will be created from the prepared + * statement. + */ + @Nullable + DriverExecutionProfile getExecutionProfileForBoundStatements(); + + /** + * The routing keyspace to use for the bound statements that will be created from the prepared + * statement. + */ + CqlIdentifier getRoutingKeyspaceForBoundStatements(); + + /** + * The routing key to use for the bound statements that will be created from the prepared + * statement. + */ + ByteBuffer getRoutingKeyForBoundStatements(); + + /** + * The routing key to use for the bound statements that will be created from the prepared + * statement. + * + *

If it's not null, it takes precedence over {@link #getRoutingKeyForBoundStatements()}. + */ + Token getRoutingTokenForBoundStatements(); + + /** + * Returns the custom payload to send alongside the bound statements that will be created from the + * prepared statement. + */ + @NonNull + Map getCustomPayloadForBoundStatements(); + + /** + * Whether bound statements that will be created from the prepared statement are idempotent. + * + *

This follows the same semantics as {@link #isIdempotent()}. + */ + @Nullable + Boolean areBoundStatementsIdempotent(); + + /** + * The timeout to use for the bound statements that will be created from the prepared statement. + * If the value is null, the default value will be used from the configuration. + */ + @Nullable + Duration getTimeoutForBoundStatements(); + + /** + * The paging state to use for the bound statements that will be created from the prepared + * statement. + */ + ByteBuffer getPagingStateForBoundStatements(); + + /** + * The page size to use for the bound statements that will be created from the prepared statement. + * If the value is 0 or negative, the default value will be used from the configuration. + */ + int getPageSizeForBoundStatements(); + + /** + * The consistency level to use for the bound statements that will be created from the prepared + * statement or {@code null} to use the default value from the configuration. + */ + @Nullable + ConsistencyLevel getConsistencyLevelForBoundStatements(); + + /** + * The serial consistency level to use for the bound statements that will be created from the + * prepared statement or {@code null} to use the default value from the configuration. + */ + @Nullable + ConsistencyLevel getSerialConsistencyLevelForBoundStatements(); + + /** Whether bound statements that will be created from the prepared statement are tracing. */ + boolean areBoundStatementsTracing(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/PreparedStatement.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/PreparedStatement.java new file mode 100644 index 00000000000..b9f9a0fdccf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/PreparedStatement.java @@ -0,0 +1,141 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * A query with bind variables that has been pre-parsed by the database. + * + *

Client applications create instances with {@link CqlSession#prepare(SimpleStatement)}. Then + * they use {@link #bind(Object...)} to obtain an executable {@link BoundStatement}. + * + *

The default prepared statement implementation returned by the driver is thread-safe. + * Client applications can -- and are expected to -- prepare each query once and store the result in + * a place where it can be accessed concurrently by application threads (for example a final field). + * Preparing the same query string twice is suboptimal and a bad practice, and will cause the driver + * to log a warning. + */ +public interface PreparedStatement { + + /** + * A unique identifier for this prepared statement. + * + *

Note: the returned buffer is read-only. + */ + @NonNull + ByteBuffer getId(); + + @NonNull + String getQuery(); + + /** A description of the bind variables of this prepared statement. */ + @NonNull + ColumnDefinitions getVariableDefinitions(); + + /** + * The indices of the variables in {@link #getVariableDefinitions()} that correspond to the target + * table's partition key. + * + *

This is only present if all the partition key columns are expressed as bind variables. + * Otherwise, the list will be empty. For example, given the following schema: + * + *

+   *   CREATE TABLE foo (pk1 int, pk2 int, cc int, v int, PRIMARY KEY ((pk1, pk2), cc));
+   * 
+ * + * And the following definitions: + * + *
+   * PreparedStatement ps1 = session.prepare("UPDATE foo SET v = ? WHERE pk1 = ? AND pk2 = ? AND v = ?");
+   * PreparedStatement ps2 = session.prepare("UPDATE foo SET v = ? WHERE pk1 = 1 AND pk2 = ? AND v = ?");
+   * 
+ * + * Then {@code ps1.getPartitionKeyIndices()} contains 1 and 2, and {@code + * ps2.getPartitionKeyIndices()} is empty (because one of the partition key components is + * hard-coded in the query string). + */ + @NonNull + List getPartitionKeyIndices(); + + /** + * A unique identifier for result metadata (essentially a hash of {@link + * #getResultSetDefinitions()}). + * + *

This information is mostly for internal use: with protocol {@link DefaultProtocolVersion#V5} + * or higher, the driver sends it with every execution of the prepared statement, to validate that + * its result metadata is still up-to-date. + * + *

Note: this method returns {@code null} for protocol {@link DefaultProtocolVersion#V4} or + * lower; otherwise, the returned buffer is read-only. + * + * @see CASSANDRA-10786 + */ + @Nullable + ByteBuffer getResultMetadataId(); + + /** + * A description of the result set that will be returned when this prepared statement is bound and + * executed. + * + *

This information is only present for {@code SELECT} queries, otherwise it is always empty. + * Note that this is slightly incorrect for conditional updates (e.g. {@code INSERT ... IF NOT + * EXISTS}), which do return columns; for those cases, use {@link + * ResultSet#getColumnDefinitions()} on the result, not this method. + */ + @NonNull + ColumnDefinitions getResultSetDefinitions(); + + /** + * Updates {@link #getResultMetadataId()} and {@link #getResultSetDefinitions()} atomically. + * + *

This is for internal use by the driver. Calling this manually with incorrect information can + * cause existing queries to fail. + */ + void setResultMetadata( + @NonNull ByteBuffer newResultMetadataId, @NonNull ColumnDefinitions newResultSetDefinitions); + + /** + * Builds an executable statement that associates a set of values with the bind variables. + * + *

Note that the built-in bound statement implementation is immutable. If you need to set + * multiple execution parameters on the bound statement (such as {@link + * BoundStatement#setExecutionProfileName(String)}, {@link + * BoundStatement#setPagingState(ByteBuffer)}, etc.), consider using {@link + * #boundStatementBuilder(Object...)} instead to avoid unnecessary allocations. + * + * @param values the values of the bound variables in the statement. You can provide less values + * than the actual number of variables (or even none at all), in which case the remaining + * variables will be left unset. However, this method will throw an {@link + * IllegalArgumentException} if there are more values than variables. Individual values can be + * {@code null}, but the vararg array itself can't. + */ + @NonNull + BoundStatement bind(@NonNull Object... values); + + /** + * Returns a builder to construct an executable statement. + * + * @see #bind(Object...) + */ + @NonNull + BoundStatementBuilder boundStatementBuilder(@NonNull Object... values); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/QueryTrace.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/QueryTrace.java new file mode 100644 index 00000000000..4af0648ce4c --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/QueryTrace.java @@ -0,0 +1,63 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetAddress; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * Tracing information for a query. + * + *

When {@link Statement#isTracing() tracing} is enabled for a query, Cassandra generates rows in + * the {@code sessions} and {@code events} table of the {@code system_traces} keyspace. This class + * is a client-side representation of that information. + */ +public interface QueryTrace { + + @NonNull + UUID getTracingId(); + + @NonNull + String getRequestType(); + + /** The server-side duration of the query in microseconds. */ + int getDurationMicros(); + + /** The IP of the node that coordinated the query. */ + @NonNull + InetAddress getCoordinator(); + + /** The parameters attached to this trace. */ + @NonNull + Map getParameters(); + + /** The server-side timestamp of the start of this query. */ + long getStartedAt(); + + /** + * The events contained in this trace. + * + *

Query tracing is asynchronous in Cassandra. Hence, it is possible for the list returned to + * be missing some events for some of the replicas involved in the query if the query trace is + * requested just after the return of the query (the only guarantee being that the list will + * contain the events pertaining to the coordinator). + */ + @NonNull + List getEvents(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/ResultSet.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/ResultSet.java new file mode 100644 index 00000000000..d4383476ca3 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/ResultSet.java @@ -0,0 +1,47 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.PagingIterable; + +/** + * The result of a synchronous CQL query. + * + *

See {@link PagingIterable} for a few generic explanations about the behavior of this object; + * in particular, implementations are not thread-safe. They can only be iterated by the + * thread that invoked {@code session.execute}. + * + * @see CqlSession#execute(Statement) + * @see CqlSession#execute(String) + */ +public interface ResultSet extends PagingIterable { + + // overridden to amend the javadocs: + /** + * {@inheritDoc} + * + *

This is equivalent to calling: + * + *

+   *   this.iterator().next().getBoolean("[applied]")
+   * 
+ * + * Except that this method peeks at the next row without consuming it. + */ + @Override + boolean wasApplied(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/Row.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/Row.java new file mode 100644 index 00000000000..9a2e88e27e8 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/Row.java @@ -0,0 +1,36 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.data.GettableById; +import com.datastax.oss.driver.api.core.data.GettableByIndex; +import com.datastax.oss.driver.api.core.data.GettableByName; +import com.datastax.oss.driver.api.core.detach.Detachable; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A row from a CQL table. + * + *

The default implementation returned by the driver is immutable and serializable. If you write + * your own implementation, it should at least be thread-safe; serializability is not mandatory, but + * recommended for use with some 3rd-party tools like Apache Spark ™. + */ +public interface Row extends GettableByIndex, GettableByName, GettableById, Detachable { + + /** @return the column definitions contained in this result set. */ + @NonNull + ColumnDefinitions getColumnDefinitions(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/SimpleStatement.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/SimpleStatement.java new file mode 100644 index 00000000000..918451b4537 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/SimpleStatement.java @@ -0,0 +1,294 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.internal.core.cql.DefaultSimpleStatement; +import com.datastax.oss.driver.internal.core.time.ServerSideTimestampGenerator; +import com.datastax.oss.driver.internal.core.util.Sizes; +import com.datastax.oss.protocol.internal.PrimitiveSizes; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableList; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.List; +import java.util.Map; + +/** + * A one-off CQL statement consisting of a query string with optional placeholders, and a set of + * values for these placeholders. + * + *

To create instances, client applications can use the {@code newInstance} factory methods on + * this interface for common cases, or {@link #builder(String)} for more control over the + * parameters. They can then be passed to {@link CqlSession#execute(Statement)}. + * + *

Simple statements should be reserved for queries that will only be executed a few times by an + * application. For more frequent queries, {@link PreparedStatement} provides many advantages: it is + * more efficient because the server parses the query only once and caches the result; it allows the + * server to return metadata about the bind variables, which allows the driver to validate the + * values earlier, and apply certain optimizations like token-aware routing. + * + *

The default implementation returned by the driver is immutable and thread-safe. + * All mutating methods return a new instance. See also the static factory methods and builders in + * this interface. + * + *

If an application reuses the same statement more than once, it is recommended to cache it (for + * example in a final field). + */ +public interface SimpleStatement extends BatchableStatement { + + /** + * Shortcut to create an instance of the default implementation with only a CQL query (see {@link + * SimpleStatementBuilder} for the defaults for the other fields). + */ + static SimpleStatement newInstance(@NonNull String cqlQuery) { + return new DefaultSimpleStatement( + cqlQuery, + NullAllowingImmutableList.of(), + NullAllowingImmutableMap.of(), + null, + null, + null, + null, + null, + null, + NullAllowingImmutableMap.of(), + null, + false, + Long.MIN_VALUE, + null, + Integer.MIN_VALUE, + null, + null, + null, + null); + } + + /** + * Shortcut to create an instance of the default implementation with only a CQL query and + * positional values (see {@link SimpleStatementBuilder} for the defaults for the other fields). + * + * @param positionalValues the values for placeholders in the query string. Individual values can + * be {@code null}, but the vararg array itself can't. + */ + static SimpleStatement newInstance( + @NonNull String cqlQuery, @NonNull Object... positionalValues) { + return new DefaultSimpleStatement( + cqlQuery, + NullAllowingImmutableList.of(positionalValues), + NullAllowingImmutableMap.of(), + null, + null, + null, + null, + null, + null, + NullAllowingImmutableMap.of(), + null, + false, + Long.MIN_VALUE, + null, + Integer.MIN_VALUE, + null, + null, + null, + null); + } + + /** + * Shortcut to create an instance of the default implementation with only a CQL query and named + * values (see {@link SimpleStatementBuilder} for the defaults for other fields). + */ + static SimpleStatement newInstance( + @NonNull String cqlQuery, @NonNull Map namedValues) { + return new DefaultSimpleStatement( + cqlQuery, + NullAllowingImmutableList.of(), + DefaultSimpleStatement.wrapKeys(namedValues), + null, + null, + null, + null, + null, + null, + NullAllowingImmutableMap.of(), + null, + false, + Long.MIN_VALUE, + null, + Integer.MIN_VALUE, + null, + null, + null, + null); + } + + /** Returns a builder to create an instance of the default implementation. */ + @NonNull + static SimpleStatementBuilder builder(@NonNull String query) { + return new SimpleStatementBuilder(query); + } + + /** + * Returns a builder to create an instance of the default implementation, copying the fields of + * the given statement. + */ + @NonNull + static SimpleStatementBuilder builder(@NonNull SimpleStatement template) { + return new SimpleStatementBuilder(template); + } + + @NonNull + String getQuery(); + + /** + * Sets the CQL query to execute. + * + *

It may contain anonymous placeholders identified by a question mark, as in: + * + *

+   *   SELECT username FROM user WHERE id = ?
+   * 
+ * + * Or named placeholders prefixed by a column, as in: + * + *
+   *   SELECT username FROM user WHERE id = :i
+   * 
+ * + *

The driver's built-in implementation is immutable, and returns a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + * + * @see #setPositionalValues(List) + * @see #setNamedValuesWithIds(Map) + */ + @NonNull + SimpleStatement setQuery(@NonNull String newQuery); + + /** + * Sets the CQL keyspace to associate with the query. + * + *

This feature is only available with {@link DefaultProtocolVersion#V5 native protocol v5} or + * higher. Specifying a per-request keyspace with lower protocol versions will cause a runtime + * error. + * + * @see Request#getKeyspace() + */ + @NonNull + SimpleStatement setKeyspace(@Nullable CqlIdentifier newKeyspace); + + /** + * Shortcut for {@link #setKeyspace(CqlIdentifier) + * setKeyspace(CqlIdentifier.fromCql(newKeyspaceName))}. + */ + @NonNull + default SimpleStatement setKeyspace(@NonNull String newKeyspaceName) { + return setKeyspace(CqlIdentifier.fromCql(newKeyspaceName)); + } + + @NonNull + List getPositionalValues(); + + /** + * Sets the positional values to bind to anonymous placeholders. + * + *

You can use either positional or named values, but not both. Therefore if you call this + * method but {@link #getNamedValues()} returns a non-empty map, an {@link + * IllegalArgumentException} will be thrown. + * + *

The driver's built-in implementation is immutable, and returns a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + * + * @see #setQuery(String) + */ + @NonNull + SimpleStatement setPositionalValues(@NonNull List newPositionalValues); + + @NonNull + Map getNamedValues(); + + /** + * Sets the named values to bind to named placeholders. + * + *

Names must be stripped of the leading column. + * + *

You can use either positional or named values, but not both. Therefore if you call this + * method but {@link #getPositionalValues()} returns a non-empty list, an {@link + * IllegalArgumentException} will be thrown. + * + *

The driver's built-in implementation is immutable, and returns a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + * + * @see #setQuery(String) + */ + @NonNull + SimpleStatement setNamedValuesWithIds(@NonNull Map newNamedValues); + + /** + * Shortcut for {@link #setNamedValuesWithIds(Map)} with raw strings as value names. The keys are + * converted on the fly with {@link CqlIdentifier#fromCql(String)}. + */ + @NonNull + default SimpleStatement setNamedValues(@NonNull Map newNamedValues) { + return setNamedValuesWithIds(DefaultSimpleStatement.wrapKeys(newNamedValues)); + } + + @Override + default int computeSizeInBytes(@NonNull DriverContext context) { + int size = Sizes.minimumStatementSize(this, context); + + // SimpleStatement's additional elements to take into account are: + // - query string + // - parameters (named or not) + // - per-query keyspace + // - page size + // - paging state + // - timestamp + + // query + size += PrimitiveSizes.sizeOfLongString(getQuery()); + + // parameters + size += + Sizes.sizeOfSimpleStatementValues( + this, context.getProtocolVersion(), context.getCodecRegistry()); + + // per-query keyspace + if (getKeyspace() != null) { + size += PrimitiveSizes.sizeOfString(getKeyspace().asInternal()); + } + + // page size + size += PrimitiveSizes.INT; + + // paging state + if (getPagingState() != null) { + size += PrimitiveSizes.sizeOfBytes(getPagingState()); + } + + // timestamp + if (!(context.getTimestampGenerator() instanceof ServerSideTimestampGenerator) + || getQueryTimestamp() != Long.MIN_VALUE) { + size += PrimitiveSizes.LONG; + } + + return size; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/SimpleStatementBuilder.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/SimpleStatementBuilder.java new file mode 100644 index 00000000000..4a1a9e32233 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/SimpleStatementBuilder.java @@ -0,0 +1,182 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.core.cql.DefaultSimpleStatement; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableList; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.NotThreadSafe; + +@NotThreadSafe +public class SimpleStatementBuilder + extends StatementBuilder { + + @NonNull private String query; + @Nullable private CqlIdentifier keyspace; + @Nullable private NullAllowingImmutableList.Builder positionalValuesBuilder; + @Nullable private NullAllowingImmutableMap.Builder namedValuesBuilder; + + public SimpleStatementBuilder(@NonNull String query) { + this.query = query; + } + + public SimpleStatementBuilder(@NonNull SimpleStatement template) { + super(template); + if (!template.getPositionalValues().isEmpty() && !template.getNamedValues().isEmpty()) { + throw new IllegalArgumentException( + "Illegal statement to copy, can't have both named and positional values"); + } + + this.query = template.getQuery(); + if (!template.getPositionalValues().isEmpty()) { + this.positionalValuesBuilder = + NullAllowingImmutableList.builder(template.getPositionalValues().size()) + .addAll(template.getPositionalValues()); + } + if (!template.getNamedValues().isEmpty()) { + this.namedValuesBuilder = + NullAllowingImmutableMap.builder(template.getNamedValues().size()) + .putAll(template.getNamedValues()); + } + } + + /** @see SimpleStatement#getQuery() */ + @NonNull + public SimpleStatementBuilder setQuery(@NonNull String query) { + this.query = query; + return this; + } + + /** @see SimpleStatement#getKeyspace() */ + @NonNull + public SimpleStatementBuilder setKeyspace(@Nullable CqlIdentifier keyspace) { + this.keyspace = keyspace; + return this; + } + + /** + * Shortcut for {@link #setKeyspace(CqlIdentifier) + * setKeyspace(CqlIdentifier.fromCql(keyspaceName))}. + */ + @NonNull + public SimpleStatementBuilder setKeyspace(@Nullable String keyspaceName) { + return setKeyspace(keyspaceName == null ? null : CqlIdentifier.fromCql(keyspaceName)); + } + + /** @see SimpleStatement#setPositionalValues(List) */ + @NonNull + public SimpleStatementBuilder addPositionalValue(@Nullable Object value) { + if (namedValuesBuilder != null) { + throw new IllegalArgumentException( + "Can't have both positional and named values in a statement."); + } + if (positionalValuesBuilder == null) { + positionalValuesBuilder = NullAllowingImmutableList.builder(); + } + positionalValuesBuilder.add(value); + return this; + } + + /** @see SimpleStatement#setPositionalValues(List) */ + @NonNull + public SimpleStatementBuilder addPositionalValues(@NonNull Iterable values) { + if (namedValuesBuilder != null) { + throw new IllegalArgumentException( + "Can't have both positional and named values in a statement."); + } + if (positionalValuesBuilder == null) { + positionalValuesBuilder = NullAllowingImmutableList.builder(); + } + positionalValuesBuilder.addAll(values); + return this; + } + + /** @see SimpleStatement#setPositionalValues(List) */ + @NonNull + public SimpleStatementBuilder addPositionalValues(@NonNull Object... values) { + return addPositionalValues(Arrays.asList(values)); + } + + /** @see SimpleStatement#setPositionalValues(List) */ + @NonNull + public SimpleStatementBuilder clearPositionalValues() { + positionalValuesBuilder = NullAllowingImmutableList.builder(); + return this; + } + + /** @see SimpleStatement#setNamedValuesWithIds(Map) */ + @NonNull + public SimpleStatementBuilder addNamedValue(@NonNull CqlIdentifier name, @Nullable Object value) { + if (positionalValuesBuilder != null) { + throw new IllegalArgumentException( + "Can't have both positional and named values in a statement."); + } + if (namedValuesBuilder == null) { + namedValuesBuilder = NullAllowingImmutableMap.builder(); + } + namedValuesBuilder.put(name, value); + return this; + } + + /** + * Shortcut for {@link #addNamedValue(CqlIdentifier, Object) + * addNamedValue(CqlIdentifier.fromCql(name), value)}. + */ + @NonNull + public SimpleStatementBuilder addNamedValue(@NonNull String name, @Nullable Object value) { + return addNamedValue(CqlIdentifier.fromCql(name), value); + } + + /** @see SimpleStatement#setNamedValuesWithIds(Map) */ + @NonNull + public SimpleStatementBuilder clearNamedValues() { + namedValuesBuilder = NullAllowingImmutableMap.builder(); + return this; + } + + @NonNull + @Override + public SimpleStatement build() { + return new DefaultSimpleStatement( + query, + (positionalValuesBuilder == null) + ? NullAllowingImmutableList.of() + : positionalValuesBuilder.build(), + (namedValuesBuilder == null) ? NullAllowingImmutableMap.of() : namedValuesBuilder.build(), + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + buildCustomPayload(), + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/Statement.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/Statement.java new file mode 100644 index 00000000000..c06b24e1982 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/Statement.java @@ -0,0 +1,377 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.NoNodeAvailableException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.time.TimestampGenerator; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.util.RoutingKey; +import edu.umd.cs.findbugs.annotations.CheckReturnValue; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +/** + * A request to execute a CQL query. + * + * @param the "self type" used for covariant returns in subtypes. + */ +public interface Statement> extends Request { + // Implementation note: "CqlRequest" would be a better name, but we keep "Statement" to match + // previous driver versions. + + /** + * The type returned when a CQL statement is executed synchronously. + * + *

Most users won't use this explicitly. It is needed for the generic execute method ({@link + * Session#execute(Request, GenericType)}), but CQL statements will generally be run with one of + * the driver's built-in helper methods (such as {@link CqlSession#execute(Statement)}). + */ + GenericType SYNC = GenericType.of(ResultSet.class); + + /** + * The type returned when a CQL statement is executed asynchronously. + * + *

Most users won't use this explicitly. It is needed for the generic execute method ({@link + * Session#execute(Request, GenericType)}), but CQL statements will generally be run with one of + * the driver's built-in helper methods (such as {@link CqlSession#executeAsync(Statement)}). + */ + GenericType> ASYNC = + new GenericType>() {}; + + /** + * Sets the name of the execution profile that will be used for this statement. + * + *

For all the driver's built-in implementations, this method has no effect if {@link + * #setExecutionProfile(DriverExecutionProfile)} has been called with a non-null argument. + * + *

All the driver's built-in implementations are immutable, and return a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + */ + @NonNull + @CheckReturnValue + SelfT setExecutionProfileName(@Nullable String newConfigProfileName); + + /** + * Sets the execution profile to use for this statement. + * + *

All the driver's built-in implementations are immutable, and return a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + */ + @NonNull + @CheckReturnValue + SelfT setExecutionProfile(@Nullable DriverExecutionProfile newProfile); + + /** + * Sets the keyspace to use for token-aware routing. + * + *

See {@link Request#getRoutingKey()} for a description of the token-aware routing algorithm. + * + * @param newRoutingKeyspace The keyspace to use, or {@code null} to disable token-aware routing. + */ + @NonNull + @CheckReturnValue + SelfT setRoutingKeyspace(@Nullable CqlIdentifier newRoutingKeyspace); + + /** + * Sets the {@link Node} that should handle this query. + * + *

In the general case, use of this method is heavily discouraged and should only be + * used in the following cases: + * + *

    + *
  1. Querying node-local tables, such as tables in the {@code system} and {@code system_views} + * keyspaces. + *
  2. Applying a series of schema changes, where it may be advantageous to execute schema + * changes in sequence on the same node. + *
+ * + *

Configuring a specific node causes the configured {@link LoadBalancingPolicy} to be + * completely bypassed. However, if the load balancing policy dictates that the node is at + * distance {@link NodeDistance#IGNORED} or there is no active connectivity to the node, the + * request will fail with a {@link NoNodeAvailableException}. + * + * @param node The node that should be used to handle executions of this statement or null to + * delegate to the configured load balancing policy. + */ + @NonNull + @CheckReturnValue + SelfT setNode(@Nullable Node node); + + /** + * Shortcut for {@link #setRoutingKeyspace(CqlIdentifier) + * setRoutingKeyspace(CqlIdentifier.fromCql(newRoutingKeyspaceName))}. + * + * @param newRoutingKeyspaceName The keyspace to use, or {@code null} to disable token-aware + * routing. + */ + @NonNull + @CheckReturnValue + default SelfT setRoutingKeyspace(@Nullable String newRoutingKeyspaceName) { + return setRoutingKeyspace( + newRoutingKeyspaceName == null ? null : CqlIdentifier.fromCql(newRoutingKeyspaceName)); + } + + /** + * Sets the key to use for token-aware routing. + * + *

See {@link Request#getRoutingKey()} for a description of the token-aware routing algorithm. + * + * @param newRoutingKey The routing key to use, or {@code null} to disable token-aware routing. + */ + @NonNull + @CheckReturnValue + SelfT setRoutingKey(@Nullable ByteBuffer newRoutingKey); + + /** + * Sets the key to use for token-aware routing, when the partition key has multiple components. + * + *

This method assembles the components into a single byte buffer and passes it to {@link + * #setRoutingKey(ByteBuffer)}. Neither the individual components, nor the vararg array itself, + * can be {@code null}. + */ + @NonNull + @CheckReturnValue + default SelfT setRoutingKey(@NonNull ByteBuffer... newRoutingKeyComponents) { + return setRoutingKey(RoutingKey.compose(newRoutingKeyComponents)); + } + + /** + * Sets the token to use for token-aware routing. + * + *

See {@link Request#getRoutingKey()} for a description of the token-aware routing algorithm. + * + * @param newRoutingToken The routing token to use, or {@code null} to disable token-aware + * routing. + */ + @NonNull + @CheckReturnValue + SelfT setRoutingToken(@Nullable Token newRoutingToken); + + /** + * Sets the custom payload to use for execution. + * + *

All the driver's built-in statement implementations are immutable, and return a new instance + * from this method. However custom implementations may choose to be mutable and return the same + * instance. + * + *

Note that it's your responsibility to provide a thread-safe map. This can be achieved with a + * concurrent or immutable implementation, or by making it effectively immutable (meaning that + * it's never modified after being set on the statement). + */ + @NonNull + @CheckReturnValue + SelfT setCustomPayload(@NonNull Map newCustomPayload); + + /** + * Sets the idempotence to use for execution. + * + *

All the driver's built-in implementations are immutable, and return a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + * + * @param newIdempotence a boolean instance to set a statement-specific value, or {@code null} to + * use the default idempotence defined in the configuration. + */ + @NonNull + @CheckReturnValue + SelfT setIdempotent(@Nullable Boolean newIdempotence); + + /** + * Sets tracing for execution. + * + *

All the driver's built-in implementations are immutable, and return a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + */ + @NonNull + @CheckReturnValue + SelfT setTracing(boolean newTracing); + + /** + * Returns the query timestamp, in microseconds, to send with the statement. + * + *

If this is equal to {@link Long#MIN_VALUE}, the {@link TimestampGenerator} configured for + * this driver instance will be used to generate a timestamp. + * + * @see TimestampGenerator + */ + long getQueryTimestamp(); + + /** + * Sets the query timestamp, in microseconds, to send with the statement. + * + *

If this is equal to {@link Long#MIN_VALUE}, the {@link TimestampGenerator} configured for + * this driver instance will be used to generate a timestamp. + * + *

All the driver's built-in implementations are immutable, and return a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance. + * + * @see TimestampGenerator + */ + @NonNull + @CheckReturnValue + SelfT setQueryTimestamp(long newTimestamp); + + /** + * Sets how long to wait for this request to complete. This is a global limit on the duration of a + * session.execute() call, including any retries the driver might do. + * + * @param newTimeout the timeout to use, or {@code null} to use the default value defined in the + * configuration. + * @see DefaultDriverOption#REQUEST_TIMEOUT + */ + @NonNull + @CheckReturnValue + SelfT setTimeout(@Nullable Duration newTimeout); + + /** + * Returns the paging state to send with the statement, or {@code null} if this statement has no + * paging state. + * + *

Paging states are used in scenarios where a paged result is interrupted then resumed later. + * The paging state can only be reused with the exact same statement (same query string, same + * parameters). It is an opaque value that is only meant to be collected, stored and re-used. If + * you try to modify its contents or reuse it with a different statement, the results are + * unpredictable. + */ + @Nullable + ByteBuffer getPagingState(); + + /** + * Sets the paging state to send with the statement, or {@code null} if this statement has no + * paging state. + * + *

Paging states are used in scenarios where a paged result is interrupted then resumed later. + * The paging state can only be reused with the exact same statement (same query string, same + * parameters). It is an opaque value that is only meant to be collected, stored and re-used. If + * you try to modify its contents or reuse it with a different statement, the results are + * unpredictable. + * + *

All the driver's built-in implementations are immutable, and return a new instance from this + * method. However custom implementations may choose to be mutable and return the same instance; + * if you do so, you must override {@link #copy(ByteBuffer)}. + */ + @NonNull + @CheckReturnValue + SelfT setPagingState(@Nullable ByteBuffer newPagingState); + + /** + * Returns the page size to use for the statement. + * + * @return the set page size, otherwise 0 or a negative value to use the default value defined in + * the configuration. + * @see DefaultDriverOption#REQUEST_PAGE_SIZE + */ + int getPageSize(); + + /** + * Configures how many rows will be retrieved simultaneously in a single network roundtrip (the + * goal being to avoid loading too many results in memory at the same time). + * + * @param newPageSize the page size to use, set to 0 or a negative value to use the default value + * defined in the configuration. + * @see DefaultDriverOption#REQUEST_PAGE_SIZE + */ + @NonNull + @CheckReturnValue + SelfT setPageSize(int newPageSize); + + /** + * Returns the {@link ConsistencyLevel} to use for the statement. + * + * @return the set consistency, or {@code null} to use the default value defined in the + * configuration. + * @see DefaultDriverOption#REQUEST_CONSISTENCY + */ + @Nullable + ConsistencyLevel getConsistencyLevel(); + + /** + * Sets the {@link ConsistencyLevel} to use for this statement. + * + * @param newConsistencyLevel the consistency level to use, or null to use the default value + * defined in the configuration. + * @see DefaultDriverOption#REQUEST_CONSISTENCY + */ + @NonNull + @CheckReturnValue + SelfT setConsistencyLevel(@Nullable ConsistencyLevel newConsistencyLevel); + + /** + * Returns the serial {@link ConsistencyLevel} to use for the statement. + * + * @return the set serial consistency, or {@code null} to use the default value defined in the + * configuration. + * @see DefaultDriverOption#REQUEST_SERIAL_CONSISTENCY + */ + @Nullable + ConsistencyLevel getSerialConsistencyLevel(); + + /** + * Sets the serial {@link ConsistencyLevel} to use for this statement. + * + * @param newSerialConsistencyLevel the serial consistency level to use, or null to use the + * default value defined in the configuration. + * @see DefaultDriverOption#REQUEST_SERIAL_CONSISTENCY + */ + @NonNull + @CheckReturnValue + SelfT setSerialConsistencyLevel(@Nullable ConsistencyLevel newSerialConsistencyLevel); + + /** Whether tracing information should be recorded for this statement. */ + boolean isTracing(); + + /** + * Calculates the approximate size in bytes that the statement will have when encoded. + * + *

The size might be over-estimated by a few bytes due to global options that may be defined on + * a {@link Session} but not explicitly set on the statement itself. + * + *

The result of this method is not cached, calling it will cause some encoding to be done in + * order to determine some of the statement's attributes sizes. Therefore, use this method + * sparingly in order to avoid unnecessary computation. + * + * @return the approximate number of bytes this statement will take when encoded. + */ + int computeSizeInBytes(@NonNull DriverContext context); + + /** + * Creates a new instance with a different paging state. + * + *

Since all the built-in statement implementations in the driver are immutable, this method's + * default implementation delegates to {@link #setPagingState(ByteBuffer)}. However, if you write + * your own mutable implementation, make sure it returns a different instance. + */ + @NonNull + @CheckReturnValue + default SelfT copy(@Nullable ByteBuffer newPagingState) { + return setPagingState(newPagingState); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/StatementBuilder.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/StatementBuilder.java new file mode 100644 index 00000000000..209672fa412 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/StatementBuilder.java @@ -0,0 +1,221 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Map; +import net.jcip.annotations.NotThreadSafe; + +/** + * Handle options common to all statement builders. + * + * @see SimpleStatement#builder(String) + * @see BatchStatement#builder(BatchType) + * @see PreparedStatement#boundStatementBuilder(Object...) + */ +@NotThreadSafe +public abstract class StatementBuilder< + SelfT extends StatementBuilder, StatementT extends Statement> { + + @SuppressWarnings("unchecked") + private final SelfT self = (SelfT) this; + + @Nullable protected String executionProfileName; + @Nullable protected DriverExecutionProfile executionProfile; + @Nullable protected CqlIdentifier routingKeyspace; + @Nullable protected ByteBuffer routingKey; + @Nullable protected Token routingToken; + @Nullable private NullAllowingImmutableMap.Builder customPayloadBuilder; + @Nullable protected Boolean idempotent; + protected boolean tracing; + protected long timestamp = Long.MIN_VALUE; + @Nullable protected ByteBuffer pagingState; + protected int pageSize = Integer.MIN_VALUE; + @Nullable protected ConsistencyLevel consistencyLevel; + @Nullable protected ConsistencyLevel serialConsistencyLevel; + @Nullable protected Duration timeout; + @Nullable protected Node node; + + protected StatementBuilder() { + // nothing to do + } + + protected StatementBuilder(StatementT template) { + this.executionProfileName = template.getExecutionProfileName(); + this.executionProfile = template.getExecutionProfile(); + this.routingKeyspace = template.getRoutingKeyspace(); + this.routingKey = template.getRoutingKey(); + this.routingToken = template.getRoutingToken(); + if (!template.getCustomPayload().isEmpty()) { + this.customPayloadBuilder = + NullAllowingImmutableMap.builder() + .putAll(template.getCustomPayload()); + } + this.idempotent = template.isIdempotent(); + this.tracing = template.isTracing(); + this.timestamp = template.getQueryTimestamp(); + this.pagingState = template.getPagingState(); + this.pageSize = template.getPageSize(); + this.consistencyLevel = template.getConsistencyLevel(); + this.serialConsistencyLevel = template.getSerialConsistencyLevel(); + this.timeout = template.getTimeout(); + this.node = template.getNode(); + } + + /** @see Statement#setExecutionProfileName(String) */ + @NonNull + public SelfT setExecutionProfileName(@Nullable String executionProfileName) { + this.executionProfileName = executionProfileName; + return self; + } + + /** @see Statement#setExecutionProfile(DriverExecutionProfile) */ + @NonNull + public SelfT setExecutionProfile(@Nullable DriverExecutionProfile executionProfile) { + this.executionProfile = executionProfile; + this.executionProfileName = null; + return self; + } + + /** @see Statement#setRoutingKeyspace(CqlIdentifier) */ + @NonNull + public SelfT setRoutingKeyspace(@Nullable CqlIdentifier routingKeyspace) { + this.routingKeyspace = routingKeyspace; + return self; + } + + /** + * Shortcut for {@link #setRoutingKeyspace(CqlIdentifier) + * setRoutingKeyspace(CqlIdentifier.fromCql(routingKeyspaceName))}. + */ + @NonNull + public SelfT setRoutingKeyspace(@Nullable String routingKeyspaceName) { + return setRoutingKeyspace( + routingKeyspaceName == null ? null : CqlIdentifier.fromCql(routingKeyspaceName)); + } + + /** @see Statement#setRoutingKey(ByteBuffer) */ + @NonNull + public SelfT setRoutingKey(@Nullable ByteBuffer routingKey) { + this.routingKey = routingKey; + return self; + } + + /** @see Statement#setRoutingToken(Token) */ + @NonNull + public SelfT setRoutingToken(@Nullable Token routingToken) { + this.routingToken = routingToken; + return self; + } + + /** @see Statement#setCustomPayload(Map) */ + @NonNull + public SelfT addCustomPayload(@NonNull String key, @Nullable ByteBuffer value) { + if (customPayloadBuilder == null) { + customPayloadBuilder = NullAllowingImmutableMap.builder(); + } + customPayloadBuilder.put(key, value); + return self; + } + + /** @see Statement#setCustomPayload(Map) */ + @NonNull + public SelfT clearCustomPayload() { + customPayloadBuilder = null; + return self; + } + + /** @see Statement#setIdempotent(Boolean) */ + @NonNull + public SelfT setIdempotence(@Nullable Boolean idempotent) { + this.idempotent = idempotent; + return self; + } + + /** @see Statement#setTracing(boolean) */ + @NonNull + public SelfT setTracing() { + this.tracing = true; + return self; + } + + /** @see Statement#setQueryTimestamp(long) */ + @NonNull + public SelfT setQueryTimestamp(long timestamp) { + this.timestamp = timestamp; + return self; + } + + /** @see Statement#setPagingState(ByteBuffer) */ + @NonNull + public SelfT setPagingState(@Nullable ByteBuffer pagingState) { + this.pagingState = pagingState; + return self; + } + + /** @see Statement#setPageSize(int) */ + @NonNull + public SelfT setPageSize(int pageSize) { + this.pageSize = pageSize; + return self; + } + + /** @see Statement#setConsistencyLevel(ConsistencyLevel) */ + @NonNull + public SelfT setConsistencyLevel(@Nullable ConsistencyLevel consistencyLevel) { + this.consistencyLevel = consistencyLevel; + return self; + } + + /** @see Statement#setSerialConsistencyLevel(ConsistencyLevel) */ + @NonNull + public SelfT setSerialConsistencyLevel(@Nullable ConsistencyLevel serialConsistencyLevel) { + this.serialConsistencyLevel = serialConsistencyLevel; + return self; + } + + /** @see Statement#setTimeout(Duration) */ + @NonNull + public SelfT setTimeout(@Nullable Duration timeout) { + this.timeout = timeout; + return self; + } + + /** @see Statement#setNode(Node) */ + public SelfT setNode(@Nullable Node node) { + this.node = node; + return self; + } + + @NonNull + protected Map buildCustomPayload() { + return (customPayloadBuilder == null) + ? NullAllowingImmutableMap.of() + : customPayloadBuilder.build(); + } + + @NonNull + public abstract StatementT build(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/cql/TraceEvent.java b/core/src/main/java/com/datastax/oss/driver/api/core/cql/TraceEvent.java new file mode 100644 index 00000000000..c55e874cdc1 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/cql/TraceEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.cql; + +import edu.umd.cs.findbugs.annotations.Nullable; +import java.net.InetAddress; + +/** An event in a {@link QueryTrace}. */ +public interface TraceEvent { + + /** Which activity this event corresponds to. */ + @Nullable + String getActivity(); + + /** The server-side timestamp of the event. */ + long getTimestamp(); + + /** The IP of the host having generated this event. */ + @Nullable + InetAddress getSource(); + + /** + * The number of microseconds elapsed on the source when this event occurred since the moment when + * the source started handling the query. + */ + int getSourceElapsedMicros(); + + /** The name of the thread on which this event occurred. */ + @Nullable + String getThreadName(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/AccessibleById.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/AccessibleById.java new file mode 100644 index 00000000000..f4cedb77c31 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/AccessibleById.java @@ -0,0 +1,47 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A data structure where the values are accessible via a CQL identifier. + * + *

In the driver, these data structures are always accessible by index as well. + */ +public interface AccessibleById extends AccessibleByIndex { + + /** + * Returns the first index where a given identifier appears (depending on the implementation, + * identifiers may appear multiple times). + * + * @throws IllegalArgumentException if the id is invalid. + */ + int firstIndexOf(@NonNull CqlIdentifier id); + + /** + * Returns the CQL type of the value for the first occurrence of {@code id}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + DataType getType(@NonNull CqlIdentifier id); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/AccessibleByIndex.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/AccessibleByIndex.java new file mode 100644 index 00000000000..509dd4866f9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/AccessibleByIndex.java @@ -0,0 +1,34 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.type.DataType; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** A data structure where the values are accessible via an integer index. */ +public interface AccessibleByIndex extends Data { + + /** Returns the number of values. */ + int size(); + + /** + * Returns the CQL type of the {@code i}th value. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + DataType getType(int i); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/AccessibleByName.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/AccessibleByName.java new file mode 100644 index 00000000000..ed7359b9c3e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/AccessibleByName.java @@ -0,0 +1,63 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A data structure where the values are accessible via a name string. + * + *

This is an optimized version of {@link AccessibleById}, in case the overhead of having to + * create a {@link CqlIdentifier} for each value is too much. + * + *

By default, case is ignored when matching names. If multiple names only differ by their case, + * then the first one is chosen. You can force an exact match by double-quoting the name. + * + *

For example, if the data structure contains three values named {@code Foo}, {@code foo} and + * {@code fOO}, then: + * + *

    + *
  • {@code getString("foo")} retrieves the first value (ignore case, first occurrence). + *
  • {@code getString("\"foo\"")} retrieves the second value (exact case). + *
  • {@code getString("\"fOO\"")} retrieves the third value (exact case). + *
  • {@code getString("\"FOO\"")} fails (exact case, no match). + *
+ * + *

In the driver, these data structures are always accessible by index as well. + */ +public interface AccessibleByName extends AccessibleByIndex { + + /** + * Returns the first index where a given identifier appears (depending on the implementation, + * identifiers may appear multiple times). + * + * @throws IllegalArgumentException if the name is invalid. + */ + int firstIndexOf(@NonNull String name); + + /** + * Returns the CQL type of the value for the first occurrence of {@code name}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * GettableByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + DataType getType(@NonNull String name); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/CqlDuration.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/CqlDuration.java new file mode 100644 index 00000000000..1db7e1d8d4f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/CqlDuration.java @@ -0,0 +1,657 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.base.Objects; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import java.time.Period; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; +import java.time.temporal.UnsupportedTemporalTypeException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.jcip.annotations.Immutable; + +/** + * A duration, as defined in CQL. + * + *

It stores months, days, and seconds separately due to the fact that the number of days in a + * month varies, and a day can have 23 or 25 hours if a daylight saving is involved. As such, this + * type differs from {@link java.time.Duration} (which only represents an amount between two points + * in time, regardless of the calendar). + */ +@Immutable +public final class CqlDuration implements TemporalAmount { + + @VisibleForTesting static final long NANOS_PER_MICRO = 1000L; + @VisibleForTesting static final long NANOS_PER_MILLI = 1000 * NANOS_PER_MICRO; + @VisibleForTesting static final long NANOS_PER_SECOND = 1000 * NANOS_PER_MILLI; + @VisibleForTesting static final long NANOS_PER_MINUTE = 60 * NANOS_PER_SECOND; + @VisibleForTesting static final long NANOS_PER_HOUR = 60 * NANOS_PER_MINUTE; + @VisibleForTesting static final int DAYS_PER_WEEK = 7; + @VisibleForTesting static final int MONTHS_PER_YEAR = 12; + + /** The Regexp used to parse the duration provided as String. */ + private static final Pattern STANDARD_PATTERN = + Pattern.compile( + "\\G(\\d+)(y|Y|mo|MO|mO|Mo|w|W|d|D|h|H|s|S|ms|MS|mS|Ms|us|US|uS|Us|µs|µS|ns|NS|nS|Ns|m|M)"); + + /** + * The Regexp used to parse the duration when provided in the ISO 8601 format with designators. + */ + private static final Pattern ISO8601_PATTERN = + Pattern.compile("P((\\d+)Y)?((\\d+)M)?((\\d+)D)?(T((\\d+)H)?((\\d+)M)?((\\d+)S)?)?"); + + /** + * The Regexp used to parse the duration when provided in the ISO 8601 format with designators. + */ + private static final Pattern ISO8601_WEEK_PATTERN = Pattern.compile("P(\\d+)W"); + + /** The Regexp used to parse the duration when provided in the ISO 8601 alternative format. */ + private static final Pattern ISO8601_ALTERNATIVE_PATTERN = + Pattern.compile("P(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})"); + + private static final ImmutableList TEMPORAL_UNITS = + ImmutableList.of(ChronoUnit.MONTHS, ChronoUnit.DAYS, ChronoUnit.NANOS); + + private final int months; + private final int days; + private final long nanoseconds; + + private CqlDuration(int months, int days, long nanoseconds) { + // Makes sure that all the values are negative if one of them is + if ((months < 0 || days < 0 || nanoseconds < 0) + && ((months > 0 || days > 0 || nanoseconds > 0))) { + throw new IllegalArgumentException( + String.format( + "All values must be either negative or positive, got %d months, %d days, %d nanoseconds", + months, days, nanoseconds)); + } + this.months = months; + this.days = days; + this.nanoseconds = nanoseconds; + } + + /** + * Creates a duration with the given number of months, days and nanoseconds. + * + *

A duration can be negative. In this case, all the non zero values must be negative. + * + * @param months the number of months + * @param days the number of days + * @param nanoseconds the number of nanoseconds + * @throws IllegalArgumentException if the values are not all negative or all positive + */ + public static CqlDuration newInstance(int months, int days, long nanoseconds) { + return new CqlDuration(months, days, nanoseconds); + } + + /** + * Converts a String into a duration. + * + *

The accepted formats are: + * + *

    + *
  • multiple digits followed by a time unit like: 12h30m where the time unit can be: + *
      + *
    • {@code y}: years + *
    • {@code m}: months + *
    • {@code w}: weeks + *
    • {@code d}: days + *
    • {@code h}: hours + *
    • {@code m}: minutes + *
    • {@code s}: seconds + *
    • {@code ms}: milliseconds + *
    • {@code us} or {@code µs}: microseconds + *
    • {@code ns}: nanoseconds + *
    + *
  • ISO 8601 format: P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W + *
  • ISO 8601 alternative format: P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss] + *
+ * + * @param input the String to convert + */ + public static CqlDuration from(@NonNull String input) { + boolean isNegative = input.startsWith("-"); + String source = isNegative ? input.substring(1) : input; + + if (source.startsWith("P")) { + if (source.endsWith("W")) { + return parseIso8601WeekFormat(isNegative, source); + } + if (source.contains("-")) { + return parseIso8601AlternativeFormat(isNegative, source); + } + return parseIso8601Format(isNegative, source); + } + return parseStandardFormat(isNegative, source); + } + + private static CqlDuration parseIso8601Format(boolean isNegative, @NonNull String source) { + Matcher matcher = ISO8601_PATTERN.matcher(source); + if (!matcher.matches()) + throw new IllegalArgumentException( + String.format("Unable to convert '%s' to a duration", source)); + + Builder builder = new Builder(isNegative); + if (matcher.group(1) != null) { + builder.addYears(groupAsLong(matcher, 2)); + } + if (matcher.group(3) != null) { + builder.addMonths(groupAsLong(matcher, 4)); + } + if (matcher.group(5) != null) { + builder.addDays(groupAsLong(matcher, 6)); + } + // Checks if the String contains time information + if (matcher.group(7) != null) { + if (matcher.group(8) != null) { + builder.addHours(groupAsLong(matcher, 9)); + } + if (matcher.group(10) != null) { + builder.addMinutes(groupAsLong(matcher, 11)); + } + if (matcher.group(12) != null) { + builder.addSeconds(groupAsLong(matcher, 13)); + } + } + return builder.build(); + } + + private static CqlDuration parseIso8601AlternativeFormat( + boolean isNegative, @NonNull String source) { + Matcher matcher = ISO8601_ALTERNATIVE_PATTERN.matcher(source); + if (!matcher.matches()) { + throw new IllegalArgumentException( + String.format("Unable to convert '%s' to a duration", source)); + } + return new Builder(isNegative) + .addYears(groupAsLong(matcher, 1)) + .addMonths(groupAsLong(matcher, 2)) + .addDays(groupAsLong(matcher, 3)) + .addHours(groupAsLong(matcher, 4)) + .addMinutes(groupAsLong(matcher, 5)) + .addSeconds(groupAsLong(matcher, 6)) + .build(); + } + + private static CqlDuration parseIso8601WeekFormat(boolean isNegative, @NonNull String source) { + Matcher matcher = ISO8601_WEEK_PATTERN.matcher(source); + if (!matcher.matches()) { + throw new IllegalArgumentException( + String.format("Unable to convert '%s' to a duration", source)); + } + return new Builder(isNegative).addWeeks(groupAsLong(matcher, 1)).build(); + } + + private static CqlDuration parseStandardFormat(boolean isNegative, @NonNull String source) { + Matcher matcher = STANDARD_PATTERN.matcher(source); + if (!matcher.find()) { + throw new IllegalArgumentException( + String.format("Unable to convert '%s' to a duration", source)); + } + Builder builder = new Builder(isNegative); + boolean done; + + do { + long number = groupAsLong(matcher, 1); + String symbol = matcher.group(2); + add(builder, number, symbol); + done = matcher.end() == source.length(); + } while (matcher.find()); + + if (!done) { + throw new IllegalArgumentException( + String.format("Unable to convert '%s' to a duration", source)); + } + return builder.build(); + } + + private static long groupAsLong(@NonNull Matcher matcher, int group) { + return Long.parseLong(matcher.group(group)); + } + + private static Builder add(@NonNull Builder builder, long number, @NonNull String symbol) { + String s = symbol.toLowerCase(); + if (s.equals("y")) { + return builder.addYears(number); + } else if (s.equals("mo")) { + return builder.addMonths(number); + } else if (s.equals("w")) { + return builder.addWeeks(number); + } else if (s.equals("d")) { + return builder.addDays(number); + } else if (s.equals("h")) { + return builder.addHours(number); + } else if (s.equals("m")) { + return builder.addMinutes(number); + } else if (s.equals("s")) { + return builder.addSeconds(number); + } else if (s.equals("ms")) { + return builder.addMillis(number); + } else if (s.equals("us") || s.equals("µs")) { + return builder.addMicros(number); + } else if (s.equals("ns")) { + return builder.addNanos(number); + } + throw new IllegalArgumentException(String.format("Unknown duration symbol '%s'", symbol)); + } + + /** + * Appends the result of the division to the specified builder if the dividend is not zero. + * + * @param builder the builder to append to + * @param dividend the dividend + * @param divisor the divisor + * @param unit the time unit to append after the result of the division + * @return the remainder of the division + */ + private static long append( + @NonNull StringBuilder builder, long dividend, long divisor, @NonNull String unit) { + if (dividend == 0 || dividend < divisor) { + return dividend; + } + builder.append(dividend / divisor).append(unit); + return dividend % divisor; + } + + /** + * Returns the number of months in this duration. + * + * @return the number of months in this duration. + */ + public int getMonths() { + return months; + } + + /** + * Returns the number of days in this duration. + * + * @return the number of days in this duration. + */ + public int getDays() { + return days; + } + + /** + * Returns the number of nanoseconds in this duration. + * + * @return the number of months in this duration. + */ + public long getNanoseconds() { + return nanoseconds; + } + + /** + * {@inheritDoc} + * + *

This implementation converts the months and days components to a {@link Period}, and the + * nanosecond component to a {@link Duration}, and adds those two amounts to the temporal object. + * Therefore the chronology of the temporal must be either the ISO chronology or null. + * + * @see Period#addTo(Temporal) + * @see Duration#addTo(Temporal) + */ + @Override + public Temporal addTo(Temporal temporal) { + return temporal.plus(Period.of(0, months, days)).plus(Duration.ofNanos(nanoseconds)); + } + + /** + * {@inheritDoc} + * + *

This implementation converts the months and days components to a {@link Period}, and the + * nanosecond component to a {@link Duration}, and subtracts those two amounts to the temporal + * object. Therefore the chronology of the temporal must be either the ISO chronology or null. + * + * @see Period#subtractFrom(Temporal) + * @see Duration#subtractFrom(Temporal) + */ + @Override + public Temporal subtractFrom(Temporal temporal) { + return temporal.minus(Period.of(0, months, days)).minus(Duration.ofNanos(nanoseconds)); + } + + @Override + public long get(TemporalUnit unit) { + if (unit == ChronoUnit.MONTHS) { + return months; + } else if (unit == ChronoUnit.DAYS) { + return days; + } else if (unit == ChronoUnit.NANOS) { + return nanoseconds; + } else { + throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit); + } + } + + @Override + public List getUnits() { + return TEMPORAL_UNITS; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof CqlDuration) { + CqlDuration that = (CqlDuration) other; + return this.days == that.days + && this.months == that.months + && this.nanoseconds == that.nanoseconds; + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hashCode(days, months, nanoseconds); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + + if (months < 0 || days < 0 || nanoseconds < 0) { + builder.append('-'); + } + long remainder = append(builder, Math.abs(months), MONTHS_PER_YEAR, "y"); + append(builder, remainder, 1, "mo"); + + append(builder, Math.abs(days), 1, "d"); + + if (nanoseconds != 0) { + remainder = append(builder, Math.abs(nanoseconds), NANOS_PER_HOUR, "h"); + remainder = append(builder, remainder, NANOS_PER_MINUTE, "m"); + remainder = append(builder, remainder, NANOS_PER_SECOND, "s"); + remainder = append(builder, remainder, NANOS_PER_MILLI, "ms"); + remainder = append(builder, remainder, NANOS_PER_MICRO, "us"); + append(builder, remainder, 1, "ns"); + } + return builder.toString(); + } + + private static class Builder { + private final boolean isNegative; + private int months; + private int days; + private long nanoseconds; + + /** We need to make sure that the values for each units are provided in order. */ + private int currentUnitIndex; + + public Builder(boolean isNegative) { + this.isNegative = isNegative; + } + + /** + * Adds the specified amount of years. + * + * @param numberOfYears the number of years to add. + * @return this {@code Builder} + */ + @NonNull + public Builder addYears(long numberOfYears) { + validateOrder(1); + validateMonths(numberOfYears, MONTHS_PER_YEAR); + // Cast to avoid http://errorprone.info/bugpattern/NarrowingCompoundAssignment + // We could also change the method to accept an int, but keeping long allows us to keep the + // calling code generic. + months += (int) numberOfYears * MONTHS_PER_YEAR; + return this; + } + + /** + * Adds the specified amount of months. + * + * @param numberOfMonths the number of months to add. + * @return this {@code Builder} + */ + @NonNull + public Builder addMonths(long numberOfMonths) { + validateOrder(2); + validateMonths(numberOfMonths, 1); + months += (int) numberOfMonths; + return this; + } + + /** + * Adds the specified amount of weeks. + * + * @param numberOfWeeks the number of weeks to add. + * @return this {@code Builder} + */ + @NonNull + public Builder addWeeks(long numberOfWeeks) { + validateOrder(3); + validateDays(numberOfWeeks, DAYS_PER_WEEK); + days += (int) numberOfWeeks * DAYS_PER_WEEK; + return this; + } + + /** + * Adds the specified amount of days. + * + * @param numberOfDays the number of days to add. + * @return this {@code Builder} + */ + @NonNull + public Builder addDays(long numberOfDays) { + validateOrder(4); + validateDays(numberOfDays, 1); + days += (int) numberOfDays; + return this; + } + + /** + * Adds the specified amount of hours. + * + * @param numberOfHours the number of hours to add. + * @return this {@code Builder} + */ + @NonNull + public Builder addHours(long numberOfHours) { + validateOrder(5); + validateNanos(numberOfHours, NANOS_PER_HOUR); + nanoseconds += numberOfHours * NANOS_PER_HOUR; + return this; + } + + /** + * Adds the specified amount of minutes. + * + * @param numberOfMinutes the number of minutes to add. + * @return this {@code Builder} + */ + @NonNull + public Builder addMinutes(long numberOfMinutes) { + validateOrder(6); + validateNanos(numberOfMinutes, NANOS_PER_MINUTE); + nanoseconds += numberOfMinutes * NANOS_PER_MINUTE; + return this; + } + + /** + * Adds the specified amount of seconds. + * + * @param numberOfSeconds the number of seconds to add. + * @return this {@code Builder} + */ + @NonNull + public Builder addSeconds(long numberOfSeconds) { + validateOrder(7); + validateNanos(numberOfSeconds, NANOS_PER_SECOND); + nanoseconds += numberOfSeconds * NANOS_PER_SECOND; + return this; + } + + /** + * Adds the specified amount of milliseconds. + * + * @param numberOfMillis the number of milliseconds to add. + * @return this {@code Builder} + */ + @NonNull + public Builder addMillis(long numberOfMillis) { + validateOrder(8); + validateNanos(numberOfMillis, NANOS_PER_MILLI); + nanoseconds += numberOfMillis * NANOS_PER_MILLI; + return this; + } + + /** + * Adds the specified amount of microseconds. + * + * @param numberOfMicros the number of microseconds to add. + * @return this {@code Builder} + */ + @NonNull + public Builder addMicros(long numberOfMicros) { + validateOrder(9); + validateNanos(numberOfMicros, NANOS_PER_MICRO); + nanoseconds += numberOfMicros * NANOS_PER_MICRO; + return this; + } + + /** + * Adds the specified amount of nanoseconds. + * + * @param numberOfNanos the number of nanoseconds to add. + * @return this {@code Builder} + */ + @NonNull + public Builder addNanos(long numberOfNanos) { + validateOrder(10); + validateNanos(numberOfNanos, 1); + nanoseconds += numberOfNanos; + return this; + } + + /** + * Validates that the total number of months can be stored. + * + * @param units the number of units that need to be added + * @param monthsPerUnit the number of days per unit + */ + private void validateMonths(long units, int monthsPerUnit) { + validate(units, (Integer.MAX_VALUE - months) / monthsPerUnit, "months"); + } + + /** + * Validates that the total number of days can be stored. + * + * @param units the number of units that need to be added + * @param daysPerUnit the number of days per unit + */ + private void validateDays(long units, int daysPerUnit) { + validate(units, (Integer.MAX_VALUE - days) / daysPerUnit, "days"); + } + + /** + * Validates that the total number of nanoseconds can be stored. + * + * @param units the number of units that need to be added + * @param nanosPerUnit the number of nanoseconds per unit + */ + private void validateNanos(long units, long nanosPerUnit) { + validate(units, (Long.MAX_VALUE - nanoseconds) / nanosPerUnit, "nanoseconds"); + } + + /** + * Validates that the specified amount is less than the limit. + * + * @param units the number of units to check + * @param limit the limit on the number of units + * @param unitName the unit name + */ + private void validate(long units, long limit, @NonNull String unitName) { + Preconditions.checkArgument( + units <= limit, + "Invalid duration. The total number of %s must be less or equal to %s", + unitName, + Integer.MAX_VALUE); + } + + /** + * Validates that the duration values are added in the proper order. + * + * @param unitIndex the unit index (e.g. years=1, months=2, ...) + */ + private void validateOrder(int unitIndex) { + if (unitIndex == currentUnitIndex) { + throw new IllegalArgumentException( + String.format( + "Invalid duration. The %s are specified multiple times", getUnitName(unitIndex))); + } + if (unitIndex <= currentUnitIndex) { + throw new IllegalArgumentException( + String.format( + "Invalid duration. The %s should be after %s", + getUnitName(currentUnitIndex), getUnitName(unitIndex))); + } + currentUnitIndex = unitIndex; + } + + /** + * Returns the name of the unit corresponding to the specified index. + * + * @param unitIndex the unit index + * @return the name of the unit corresponding to the specified index. + */ + @NonNull + private String getUnitName(int unitIndex) { + switch (unitIndex) { + case 1: + return "years"; + case 2: + return "months"; + case 3: + return "weeks"; + case 4: + return "days"; + case 5: + return "hours"; + case 6: + return "minutes"; + case 7: + return "seconds"; + case 8: + return "milliseconds"; + case 9: + return "microseconds"; + case 10: + return "nanoseconds"; + default: + throw new AssertionError("unknown unit index: " + unitIndex); + } + } + + @NonNull + public CqlDuration build() { + return isNegative + ? new CqlDuration(-months, -days, -nanoseconds) + : new CqlDuration(months, days, nanoseconds); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/Data.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/Data.java new file mode 100644 index 00000000000..0db0a6b44e8 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/Data.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.detach.Detachable; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** A data structure containing CQL values. */ +public interface Data { + + /** + * Returns the registry of all the codecs currently available to convert values for this instance. + * + *

If you obtained this object from the driver, this will be set automatically. If you created + * it manually, or just deserialized it, it is set to {@link CodecRegistry#DEFAULT}. You can + * reattach this object to an existing driver instance to use its codec registry. + * + * @see Detachable + */ + @NonNull + CodecRegistry codecRegistry(); + + /** + * Returns the protocol version that is currently used to convert values for this instance. + * + *

If you obtained this object from the driver, this will be set automatically. If you created + * it manually, or just deserialized it, it is set to {@link DefaultProtocolVersion#DEFAULT}. You + * can reattach this object to an existing driver instance to use its protocol version. + * + * @see Detachable + */ + @NonNull + ProtocolVersion protocolVersion(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/GettableById.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/GettableById.java new file mode 100644 index 00000000000..bf0ccfe1f2b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/GettableById.java @@ -0,0 +1,646 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** A data structure that provides methods to retrieve its values via a CQL identifier. */ +public interface GettableById extends GettableByIndex, AccessibleById { + + /** + * Returns the raw binary representation of the value for the first occurrence of {@code id}. + * + *

This is primarily for internal use; you'll likely want to use one of the typed getters + * instead, to get a higher-level Java representation. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @return the raw value, or {@code null} if the CQL value is {@code NULL}. For performance + * reasons, this is the actual instance used internally. If you read data from the buffer, + * make sure to {@link ByteBuffer#duplicate() duplicate} it beforehand, or only use relative + * methods. If you change the buffer's index or its contents in any way, any other getter + * invocation for this value will have unpredictable results. + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default ByteBuffer getBytesUnsafe(@NonNull CqlIdentifier id) { + return getBytesUnsafe(firstIndexOf(id)); + } + + /** + * Indicates whether the value for the first occurrence of {@code id} is a CQL {@code NULL}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + default boolean isNull(@NonNull CqlIdentifier id) { + return isNull(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id}, using the given codec for the + * conversion. + * + *

This method completely bypasses the {@link #codecRegistry()}, and forces the driver to use + * the given codec instead. This can be useful if the codec would collide with a previously + * registered one, or if you want to use the codec just once without registering it. + * + *

It is the caller's responsibility to ensure that the given codec is appropriate for the + * conversion. Failing to do so will result in errors at runtime. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default ValueT get(@NonNull CqlIdentifier id, @NonNull TypeCodec codec) { + return get(firstIndexOf(id), codec); + } + + /** + * Returns the value for the first occurrence of {@code id}, converting it to the given Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

This variant is for generic Java types. If the target type is not generic, use {@link + * #get(int, Class)} instead, which may perform slightly better. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @Nullable + default ValueT get(@NonNull CqlIdentifier id, @NonNull GenericType targetType) { + return get(firstIndexOf(id), targetType); + } + + /** + * Returns the value for the first occurrence of {@code id}, converting it to the given Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

If the target type is generic, use {@link #get(int, GenericType)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @Nullable + default ValueT get(@NonNull CqlIdentifier id, @NonNull Class targetClass) { + return get(firstIndexOf(id), targetClass); + } + + /** + * Returns the value for the first occurrence of {@code id}, converting it to the most appropriate + * Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

Use this method to dynamically inspect elements when types aren't known in advance, for + * instance if you're writing a generic row logger. If you know the target Java type, it is + * generally preferable to use typed variants, such as the ones for built-in types ({@link + * #getBoolean(int)}, {@link #getInt(int)}, etc.), or {@link #get(int, Class)} and {@link + * #get(int, GenericType)} for custom types. + * + *

The definition of "most appropriate" is unspecified, and left to the appreciation of the + * {@link #codecRegistry()} implementation. By default, the driver uses the mapping described in + * the other {@code getXxx()} methods (for example {@link #getString(int) String for text, varchar + * and ascii}, etc). + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @Nullable + default Object getObject(@NonNull CqlIdentifier id) { + return getObject(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java primitive boolean. + * + *

By default, this works with CQL type {@code boolean}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code false}. If this doesn't work for you, either call {@link + * #isNull(CqlIdentifier)} before calling this method, or use {@code get(id, Boolean.class)} + * instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + default boolean getBoolean(@NonNull CqlIdentifier id) { + return getBoolean(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java primitive byte. + * + *

By default, this works with CQL type {@code tinyint}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(CqlIdentifier)} before calling this method, or use {@code get(id, Byte.class)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + default byte getByte(@NonNull CqlIdentifier id) { + return getByte(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java primitive double. + * + *

By default, this works with CQL type {@code double}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0.0}. If this doesn't work for you, either call {@link + * #isNull(CqlIdentifier)} before calling this method, or use {@code get(id, Double.class)} + * instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + default double getDouble(@NonNull CqlIdentifier id) { + return getDouble(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java primitive float. + * + *

By default, this works with CQL type {@code float}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0.0}. If this doesn't work for you, either call {@link + * #isNull(CqlIdentifier)} before calling this method, or use {@code get(id, Float.class)} + * instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + default float getFloat(@NonNull CqlIdentifier id) { + return getFloat(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java primitive integer. + * + *

By default, this works with CQL type {@code int}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(CqlIdentifier)} before calling this method, or use {@code get(id, Integer.class)} + * instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + default int getInt(@NonNull CqlIdentifier id) { + return getInt(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java primitive long. + * + *

By default, this works with CQL types {@code bigint} and {@code counter}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(CqlIdentifier)} before calling this method, or use {@code get(id, Long.class)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + default long getLong(@NonNull CqlIdentifier id) { + return getLong(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java primitive short. + * + *

By default, this works with CQL type {@code smallint}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(CqlIdentifier)} before calling this method, or use {@code get(id, Short.class)} + * instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + default short getShort(@NonNull CqlIdentifier id) { + return getShort(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java instant. + * + *

By default, this works with CQL type {@code timestamp}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default Instant getInstant(@NonNull CqlIdentifier id) { + return getInstant(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java local date. + * + *

By default, this works with CQL type {@code date}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default LocalDate getLocalDate(@NonNull CqlIdentifier id) { + return getLocalDate(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java local time. + * + *

By default, this works with CQL type {@code time}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default LocalTime getLocalTime(@NonNull CqlIdentifier id) { + return getLocalTime(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java byte buffer. + * + *

By default, this works with CQL type {@code blob}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default ByteBuffer getByteBuffer(@NonNull CqlIdentifier id) { + return getByteBuffer(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java string. + * + *

By default, this works with CQL types {@code text}, {@code varchar} and {@code ascii}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default String getString(@NonNull CqlIdentifier id) { + return getString(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java big integer. + * + *

By default, this works with CQL type {@code varint}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default BigInteger getBigInteger(@NonNull CqlIdentifier id) { + return getBigInteger(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java big decimal. + * + *

By default, this works with CQL type {@code decimal}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default BigDecimal getBigDecimal(@NonNull CqlIdentifier id) { + return getBigDecimal(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java UUID. + * + *

By default, this works with CQL types {@code uuid} and {@code timeuuid}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default UUID getUuid(@NonNull CqlIdentifier id) { + return getUuid(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java IP address. + * + *

By default, this works with CQL type {@code inet}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default InetAddress getInetAddress(@NonNull CqlIdentifier id) { + return getInetAddress(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a duration. + * + *

By default, this works with CQL type {@code duration}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default CqlDuration getCqlDuration(@NonNull CqlIdentifier id) { + return getCqlDuration(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a token. + * + *

Note that, for simplicity, this method relies on the CQL type of the column to pick the + * correct token implementation. Therefore it must only be called on columns of the type that + * matches the partitioner in use for this cluster: {@code bigint} for {@code Murmur3Partitioner}, + * {@code blob} for {@code ByteOrderedPartitioner}, and {@code varint} for {@code + * RandomPartitioner}. Calling it for the wrong type will produce corrupt tokens that are unusable + * with this driver instance. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the column type can not be converted to a known token type + * or if the name is invalid. + */ + @Nullable + default Token getToken(@NonNull CqlIdentifier id) { + return getToken(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java list. + * + *

By default, this works with CQL type {@code list}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex list types, use {@link #get(int, GenericType)}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + *

Apache Cassandra does not make any distinction between an empty collection and {@code null}. + * Whether this method will return an empty collection or {@code null} will depend on the codec + * used; by default, the driver's built-in codecs all return empty collections. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default List getList( + @NonNull CqlIdentifier id, @NonNull Class elementsClass) { + return getList(firstIndexOf(id), elementsClass); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java set. + * + *

By default, this works with CQL type {@code set}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex set types, use {@link #get(int, GenericType)}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + *

Apache Cassandra does not make any distinction between an empty collection and {@code null}. + * Whether this method will return an empty collection or {@code null} will depend on the codec + * used; by default, the driver's built-in codecs all return empty collections. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default Set getSet( + @NonNull CqlIdentifier id, @NonNull Class elementsClass) { + return getSet(firstIndexOf(id), elementsClass); + } + + /** + * Returns the value for the first occurrence of {@code id} as a Java map. + * + *

By default, this works with CQL type {@code map}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex map types, use {@link #get(int, GenericType)}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + *

Apache Cassandra does not make any distinction between an empty collection and {@code null}. + * Whether this method will return an empty collection or {@code null} will depend on the codec + * used; by default, the driver's built-in codecs all return empty collections. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default Map getMap( + @NonNull CqlIdentifier id, @NonNull Class keyClass, @NonNull Class valueClass) { + return getMap(firstIndexOf(id), keyClass, valueClass); + } + + /** + * Returns the value for the first occurrence of {@code id} as a user defined type value. + * + *

By default, this works with CQL user-defined types. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default UdtValue getUdtValue(@NonNull CqlIdentifier id) { + return getUdtValue(firstIndexOf(id)); + } + + /** + * Returns the value for the first occurrence of {@code id} as a tuple value. + * + *

By default, this works with CQL tuples. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @Nullable + default TupleValue getTupleValue(@NonNull CqlIdentifier id) { + return getTupleValue(firstIndexOf(id)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/GettableByIndex.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/GettableByIndex.java new file mode 100644 index 00000000000..177fd654507 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/GettableByIndex.java @@ -0,0 +1,542 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveBooleanCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveByteCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveDoubleCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveFloatCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveIntCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveLongCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveShortCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.metadata.token.ByteOrderedToken; +import com.datastax.oss.driver.internal.core.metadata.token.Murmur3Token; +import com.datastax.oss.driver.internal.core.metadata.token.RandomToken; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** A data structure that provides methods to retrieve its values via an integer index. */ +public interface GettableByIndex extends AccessibleByIndex { + + /** + * Returns the raw binary representation of the {@code i}th value. + * + *

This is primarily for internal use; you'll likely want to use one of the typed getters + * instead, to get a higher-level Java representation. + * + * @return the raw value, or {@code null} if the CQL value is {@code NULL}. For performance + * reasons, this is the actual instance used internally. If you read data from the buffer, + * make sure to {@link ByteBuffer#duplicate() duplicate} it beforehand, or only use relative + * methods. If you change the buffer's index or its contents in any way, any other getter + * invocation for this value will have unpredictable results. + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + ByteBuffer getBytesUnsafe(int i); + + /** + * Indicates whether the {@code i}th value is a CQL {@code NULL}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + default boolean isNull(int i) { + return getBytesUnsafe(i) == null; + } + + /** + * Returns the {@code i}th value, using the given codec for the conversion. + * + *

This method completely bypasses the {@link #codecRegistry()}, and forces the driver to use + * the given codec instead. This can be useful if the codec would collide with a previously + * registered one, or if you want to use the codec just once without registering it. + * + *

It is the caller's responsibility to ensure that the given codec is appropriate for the + * conversion. Failing to do so will result in errors at runtime. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default ValueT get(int i, TypeCodec codec) { + return codec.decode(getBytesUnsafe(i), protocolVersion()); + } + + /** + * Returns the {@code i}th value, converting it to the given Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

This variant is for generic Java types. If the target type is not generic, use {@link + * #get(int, Class)} instead, which may perform slightly better. + * + * @throws IndexOutOfBoundsException if the index is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @Nullable + default ValueT get(int i, GenericType targetType) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, targetType); + return get(i, codec); + } + + /** + * Returns the {@code i}th value, converting it to the given Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

If the target type is generic, use {@link #get(int, GenericType)} instead. + * + * @throws IndexOutOfBoundsException if the index is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @Nullable + default ValueT get(int i, Class targetClass) { + // This is duplicated from the GenericType variant, because we want to give the codec registry + // a chance to process the unwrapped class directly, if it can do so in a more efficient way. + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, targetClass); + return get(i, codec); + } + + /** + * Returns the {@code i}th value, converting it to the most appropriate Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

Use this method to dynamically inspect elements when types aren't known in advance, for + * instance if you're writing a generic row logger. If you know the target Java type, it is + * generally preferable to use typed variants, such as the ones for built-in types ({@link + * #getBoolean(int)}, {@link #getInt(int)}, etc.), or {@link #get(int, Class)} and {@link + * #get(int, GenericType)} for custom types. + * + *

The definition of "most appropriate" is unspecified, and left to the appreciation of the + * {@link #codecRegistry()} implementation. By default, the driver uses the mapping described in + * the other {@code getXxx()} methods (for example {@link #getString(int) String for text, varchar + * and ascii}, etc). + * + * @throws IndexOutOfBoundsException if the index is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @Nullable + default Object getObject(int i) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType); + return codec.decode(getBytesUnsafe(i), protocolVersion()); + } + + /** + * Returns the {@code i}th value as a Java primitive boolean. + * + *

By default, this works with CQL type {@code boolean}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code false}. If this doesn't work for you, either call {@link + * #isNull(int)} before calling this method, or use {@code get(i, Boolean.class)} instead. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + default boolean getBoolean(int i) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Boolean.class); + if (codec instanceof PrimitiveBooleanCodec) { + return ((PrimitiveBooleanCodec) codec).decodePrimitive(getBytesUnsafe(i), protocolVersion()); + } else { + Boolean value = get(i, codec); + return value == null ? false : value; + } + } + + /** + * Returns the {@code i}th value as a Java primitive byte. + * + *

By default, this works with CQL type {@code tinyint}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(int)} before calling this method, or use {@code get(i, Byte.class)} instead. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + default byte getByte(int i) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Byte.class); + if (codec instanceof PrimitiveByteCodec) { + return ((PrimitiveByteCodec) codec).decodePrimitive(getBytesUnsafe(i), protocolVersion()); + } else { + Byte value = get(i, codec); + return value == null ? 0 : value; + } + } + + /** + * Returns the {@code i}th value as a Java primitive double. + * + *

By default, this works with CQL type {@code double}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0.0}. If this doesn't work for you, either call {@link + * #isNull(int)} before calling this method, or use {@code get(i, Double.class)} instead. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + default double getDouble(int i) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Double.class); + if (codec instanceof PrimitiveDoubleCodec) { + return ((PrimitiveDoubleCodec) codec).decodePrimitive(getBytesUnsafe(i), protocolVersion()); + } else { + Double value = get(i, codec); + return value == null ? 0 : value; + } + } + + /** + * Returns the {@code i}th value as a Java primitive float. + * + *

By default, this works with CQL type {@code float}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0.0}. If this doesn't work for you, either call {@link + * #isNull(int)} before calling this method, or use {@code get(i, Float.class)} instead. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + default float getFloat(int i) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Float.class); + if (codec instanceof PrimitiveFloatCodec) { + return ((PrimitiveFloatCodec) codec).decodePrimitive(getBytesUnsafe(i), protocolVersion()); + } else { + Float value = get(i, codec); + return value == null ? 0 : value; + } + } + + /** + * Returns the {@code i}th value as a Java primitive integer. + * + *

By default, this works with CQL type {@code int}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(int)} before calling this method, or use {@code get(i, Integer.class)} instead. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + default int getInt(int i) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Integer.class); + if (codec instanceof PrimitiveIntCodec) { + return ((PrimitiveIntCodec) codec).decodePrimitive(getBytesUnsafe(i), protocolVersion()); + } else { + Integer value = get(i, codec); + return value == null ? 0 : value; + } + } + + /** + * Returns the {@code i}th value as a Java primitive long. + * + *

By default, this works with CQL types {@code bigint} and {@code counter}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(int)} before calling this method, or use {@code get(i, Long.class)} instead. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + default long getLong(int i) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Long.class); + if (codec instanceof PrimitiveLongCodec) { + return ((PrimitiveLongCodec) codec).decodePrimitive(getBytesUnsafe(i), protocolVersion()); + } else { + Long value = get(i, codec); + return value == null ? 0 : value; + } + } + + /** + * Returns the {@code i}th value as a Java primitive short. + * + *

By default, this works with CQL type {@code smallint}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(int)} before calling this method, or use {@code get(i, Short.class)} instead. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + default short getShort(int i) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Short.class); + if (codec instanceof PrimitiveShortCodec) { + return ((PrimitiveShortCodec) codec).decodePrimitive(getBytesUnsafe(i), protocolVersion()); + } else { + Short value = get(i, codec); + return value == null ? 0 : value; + } + } + + /** + * Returns the {@code i}th value as a Java instant. + * + *

By default, this works with CQL type {@code timestamp}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default Instant getInstant(int i) { + return get(i, Instant.class); + } + + /** + * Returns the {@code i}th value as a Java local date. + * + *

By default, this works with CQL type {@code date}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default LocalDate getLocalDate(int i) { + return get(i, LocalDate.class); + } + + /** + * Returns the {@code i}th value as a Java local time. + * + *

By default, this works with CQL type {@code time}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default LocalTime getLocalTime(int i) { + return get(i, LocalTime.class); + } + + /** + * Returns the {@code i}th value as a Java byte buffer. + * + *

By default, this works with CQL type {@code blob}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default ByteBuffer getByteBuffer(int i) { + return get(i, ByteBuffer.class); + } + + /** + * Returns the {@code i}th value as a Java string. + * + *

By default, this works with CQL types {@code text}, {@code varchar} and {@code ascii}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default String getString(int i) { + return get(i, String.class); + } + + /** + * Returns the {@code i}th value as a Java big integer. + * + *

By default, this works with CQL type {@code varint}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default BigInteger getBigInteger(int i) { + return get(i, BigInteger.class); + } + + /** + * Returns the {@code i}th value as a Java big decimal. + * + *

By default, this works with CQL type {@code decimal}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default BigDecimal getBigDecimal(int i) { + return get(i, BigDecimal.class); + } + + /** + * Returns the {@code i}th value as a Java UUID. + * + *

By default, this works with CQL types {@code uuid} and {@code timeuuid}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default UUID getUuid(int i) { + return get(i, UUID.class); + } + + /** + * Returns the {@code i}th value as a Java IP address. + * + *

By default, this works with CQL type {@code inet}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default InetAddress getInetAddress(int i) { + return get(i, InetAddress.class); + } + + /** + * Returns the {@code i}th value as a duration. + * + *

By default, this works with CQL type {@code duration}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default CqlDuration getCqlDuration(int i) { + return get(i, CqlDuration.class); + } + + /** + * Returns the {@code i}th value as a token. + * + *

Note that, for simplicity, this method relies on the CQL type of the column to pick the + * correct token implementation. Therefore it must only be called on columns of the type that + * matches the partitioner in use for this cluster: {@code bigint} for {@code Murmur3Partitioner}, + * {@code blob} for {@code ByteOrderedPartitioner}, and {@code varint} for {@code + * RandomPartitioner}. Calling it for the wrong type will produce corrupt tokens that are unusable + * with this driver instance. + * + * @throws IndexOutOfBoundsException if the index is invalid. + * @throws IllegalArgumentException if the column type can not be converted to a known token type. + */ + @Nullable + default Token getToken(int i) { + DataType type = getType(i); + // Simply enumerate all known implementations. This goes against the concept of TokenFactory, + // but injecting the factory here is too much of a hassle. + // The only issue is if someone uses a custom partitioner, but this is highly unlikely, and even + // then they can get the value manually as a workaround. + if (type.equals(DataTypes.BIGINT)) { + return isNull(i) ? null : new Murmur3Token(getLong(i)); + } else if (type.equals(DataTypes.BLOB)) { + return isNull(i) ? null : new ByteOrderedToken(getByteBuffer(i)); + } else if (type.equals(DataTypes.VARINT)) { + return isNull(i) ? null : new RandomToken(getBigInteger(i)); + } else { + throw new IllegalArgumentException("Can't convert CQL type " + type + " into a token"); + } + } + + /** + * Returns the {@code i}th value as a Java list. + * + *

By default, this works with CQL type {@code list}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex list types, use {@link #get(int, GenericType)}. + * + *

Apache Cassandra does not make any distinction between an empty collection and {@code null}. + * Whether this method will return an empty collection or {@code null} will depend on the codec + * used; by default, the driver's built-in codecs all return empty collections. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default List getList(int i, @NonNull Class elementsClass) { + return get(i, GenericType.listOf(elementsClass)); + } + + /** + * Returns the {@code i}th value as a Java set. + * + *

By default, this works with CQL type {@code set}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex set types, use {@link #get(int, GenericType)}. + * + *

Apache Cassandra does not make any distinction between an empty collection and {@code null}. + * Whether this method will return an empty collection or {@code null} will depend on the codec + * used; by default, the driver's built-in codecs all return empty collections. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default Set getSet(int i, @NonNull Class elementsClass) { + return get(i, GenericType.setOf(elementsClass)); + } + + /** + * Returns the {@code i}th value as a Java map. + * + *

By default, this works with CQL type {@code map}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex map types, use {@link #get(int, GenericType)}. + * + *

Apache Cassandra does not make any distinction between an empty collection and {@code null}. + * Whether this method will return an empty collection or {@code null} will depend on the codec + * used; by default, the driver's built-in codecs all return empty collections. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default Map getMap( + int i, @NonNull Class keyClass, @NonNull Class valueClass) { + return get(i, GenericType.mapOf(keyClass, valueClass)); + } + + /** + * Returns the {@code i}th value as a user defined type value. + * + *

By default, this works with CQL user-defined types. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default UdtValue getUdtValue(int i) { + return get(i, UdtValue.class); + } + + /** + * Returns the {@code i}th value as a tuple value. + * + *

By default, this works with CQL tuples. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @Nullable + default TupleValue getTupleValue(int i) { + return get(i, TupleValue.class); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/GettableByName.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/GettableByName.java new file mode 100644 index 00000000000..c1aca1576c6 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/GettableByName.java @@ -0,0 +1,642 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** A data structure that provides methods to retrieve its values via a name. */ +public interface GettableByName extends GettableByIndex, AccessibleByName { + + /** + * Returns the raw binary representation of the value for the first occurrence of {@code name}. + * + *

This is primarily for internal use; you'll likely want to use one of the typed getters + * instead, to get a higher-level Java representation. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @return the raw value, or {@code null} if the CQL value is {@code NULL}. For performance + * reasons, this is the actual instance used internally. If you read data from the buffer, + * make sure to {@link ByteBuffer#duplicate() duplicate} it beforehand, or only use relative + * methods. If you change the buffer's index or its contents in any way, any other getter + * invocation for this value will have unpredictable results. + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default ByteBuffer getBytesUnsafe(@NonNull String name) { + return getBytesUnsafe(firstIndexOf(name)); + } + + /** + * Indicates whether the value for the first occurrence of {@code name} is a CQL {@code NULL}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + default boolean isNull(@NonNull String name) { + return isNull(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name}, using the given codec for the + * conversion. + * + *

This method completely bypasses the {@link #codecRegistry()}, and forces the driver to use + * the given codec instead. This can be useful if the codec would collide with a previously + * registered one, or if you want to use the codec just once without registering it. + * + *

It is the caller's responsibility to ensure that the given codec is appropriate for the + * conversion. Failing to do so will result in errors at runtime. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default ValueT get(@NonNull String name, @NonNull TypeCodec codec) { + return get(firstIndexOf(name), codec); + } + + /** + * Returns the value for the first occurrence of {@code name}, converting it to the given Java + * type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

This variant is for generic Java types. If the target type is not generic, use {@link + * #get(int, Class)} instead, which may perform slightly better. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @Nullable + default ValueT get(@NonNull String name, @NonNull GenericType targetType) { + return get(firstIndexOf(name), targetType); + } + + /** + * Returns the value for the first occurrence of {@code name}, converting it to the given Java + * type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

If the target type is generic, use {@link #get(int, GenericType)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @Nullable + default ValueT get(@NonNull String name, @NonNull Class targetClass) { + return get(firstIndexOf(name), targetClass); + } + + /** + * Returns the value for the first occurrence of {@code name}, converting it to the most + * appropriate Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

Use this method to dynamically inspect elements when types aren't known in advance, for + * instance if you're writing a generic row logger. If you know the target Java type, it is + * generally preferable to use typed variants, such as the ones for built-in types ({@link + * #getBoolean(int)}, {@link #getInt(int)}, etc.), or {@link #get(int, Class)} and {@link + * #get(int, GenericType)} for custom types. + * + *

The definition of "most appropriate" is unspecified, and left to the appreciation of the + * {@link #codecRegistry()} implementation. By default, the driver uses the mapping described in + * the other {@code getXxx()} methods (for example {@link #getString(int) String for text, varchar + * and ascii}, etc). + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @Nullable + default Object getObject(@NonNull String name) { + return getObject(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java primitive boolean. + * + *

By default, this works with CQL type {@code boolean}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code false}. If this doesn't work for you, either call {@link + * #isNull(String)} before calling this method, or use {@code get(name, Boolean.class)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + default boolean getBoolean(@NonNull String name) { + return getBoolean(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java primitive byte. + * + *

By default, this works with CQL type {@code tinyint}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(String)} before calling this method, or use {@code get(name, Byte.class)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + default byte getByte(@NonNull String name) { + return getByte(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java primitive double. + * + *

By default, this works with CQL type {@code double}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0.0}. If this doesn't work for you, either call {@link + * #isNull(String)} before calling this method, or use {@code get(name, Double.class)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + default double getDouble(@NonNull String name) { + return getDouble(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java primitive float. + * + *

By default, this works with CQL type {@code float}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0.0}. If this doesn't work for you, either call {@link + * #isNull(String)} before calling this method, or use {@code get(name, Float.class)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + default float getFloat(@NonNull String name) { + return getFloat(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java primitive integer. + * + *

By default, this works with CQL type {@code int}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(String)} before calling this method, or use {@code get(name, Integer.class)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + default int getInt(@NonNull String name) { + return getInt(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java primitive long. + * + *

By default, this works with CQL types {@code bigint} and {@code counter}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(String)} before calling this method, or use {@code get(name, Long.class)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + default long getLong(@NonNull String name) { + return getLong(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java primitive short. + * + *

By default, this works with CQL type {@code smallint}. + * + *

Note that, due to its signature, this method cannot return {@code null}. If the CQL value is + * {@code NULL}, it will return {@code 0}. If this doesn't work for you, either call {@link + * #isNull(String)} before calling this method, or use {@code get(name, Short.class)} instead. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + default short getShort(@NonNull String name) { + return getShort(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java instant. + * + *

By default, this works with CQL type {@code timestamp}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default Instant getInstant(@NonNull String name) { + return getInstant(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java local date. + * + *

By default, this works with CQL type {@code date}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default LocalDate getLocalDate(@NonNull String name) { + return getLocalDate(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java local time. + * + *

By default, this works with CQL type {@code time}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default LocalTime getLocalTime(@NonNull String name) { + return getLocalTime(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java byte buffer. + * + *

By default, this works with CQL type {@code blob}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default ByteBuffer getByteBuffer(@NonNull String name) { + return getByteBuffer(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java string. + * + *

By default, this works with CQL types {@code text}, {@code varchar} and {@code ascii}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default String getString(@NonNull String name) { + return getString(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java big integer. + * + *

By default, this works with CQL type {@code varint}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default BigInteger getBigInteger(@NonNull String name) { + return getBigInteger(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java big decimal. + * + *

By default, this works with CQL type {@code decimal}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default BigDecimal getBigDecimal(@NonNull String name) { + return getBigDecimal(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java UUID. + * + *

By default, this works with CQL types {@code uuid} and {@code timeuuid}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default UUID getUuid(@NonNull String name) { + return getUuid(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java IP address. + * + *

By default, this works with CQL type {@code inet}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default InetAddress getInetAddress(@NonNull String name) { + return getInetAddress(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a duration. + * + *

By default, this works with CQL type {@code duration}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default CqlDuration getCqlDuration(@NonNull String name) { + return getCqlDuration(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a token. + * + *

Note that, for simplicity, this method relies on the CQL type of the column to pick the + * correct token implementation. Therefore it must only be called on columns of the type that + * matches the partitioner in use for this cluster: {@code bigint} for {@code Murmur3Partitioner}, + * {@code blob} for {@code ByteOrderedPartitioner}, and {@code varint} for {@code + * RandomPartitioner}. Calling it for the wrong type will produce corrupt tokens that are unusable + * with this driver instance. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the column type can not be converted to a known token type + * or if the name is invalid. + */ + @Nullable + default Token getToken(@NonNull String name) { + return getToken(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java list. + * + *

By default, this works with CQL type {@code list}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex list types, use {@link #get(int, GenericType)}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + *

Apache Cassandra does not make any distinction between an empty collection and {@code null}. + * Whether this method will return an empty collection or {@code null} will depend on the codec + * used; by default, the driver's built-in codecs all return empty collections. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default List getList( + @NonNull String name, @NonNull Class elementsClass) { + return getList(firstIndexOf(name), elementsClass); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java set. + * + *

By default, this works with CQL type {@code set}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex set types, use {@link #get(int, GenericType)}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + *

Apache Cassandra does not make any distinction between an empty collection and {@code null}. + * Whether this method will return an empty collection or {@code null} will depend on the codec + * used; by default, the driver's built-in codecs all return empty collections. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default Set getSet( + @NonNull String name, @NonNull Class elementsClass) { + return getSet(firstIndexOf(name), elementsClass); + } + + /** + * Returns the value for the first occurrence of {@code name} as a Java map. + * + *

By default, this works with CQL type {@code map}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex map types, use {@link #get(int, GenericType)}. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + *

Apache Cassandra does not make any distinction between an empty collection and {@code null}. + * Whether this method will return an empty collection or {@code null} will depend on the codec + * used; by default, the driver's built-in codecs all return empty collections. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default Map getMap( + @NonNull String name, @NonNull Class keyClass, @NonNull Class valueClass) { + return getMap(firstIndexOf(name), keyClass, valueClass); + } + + /** + * Returns the value for the first occurrence of {@code name} as a user defined type value. + * + *

By default, this works with CQL user-defined types. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default UdtValue getUdtValue(@NonNull String name) { + return getUdtValue(firstIndexOf(name)); + } + + /** + * Returns the value for the first occurrence of {@code name} as a tuple value. + * + *

By default, this works with CQL tuples. + * + *

If an identifier appears multiple times, this can only be used to access the first value. + * For the other ones, use positional getters. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @Nullable + default TupleValue getTupleValue(@NonNull String name) { + return getTupleValue(firstIndexOf(name)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/SettableById.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/SettableById.java new file mode 100644 index 00000000000..de9a906ca49 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/SettableById.java @@ -0,0 +1,558 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.CheckReturnValue; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** A data structure that provides methods to set its values via a CQL identifier. */ +public interface SettableById> + extends SettableByIndex, AccessibleById { + + /** + * Sets the raw binary representation of the value for the first occurrence of {@code id}. + * + *

This is primarily for internal use; you'll likely want to use one of the typed setters + * instead, to pass a higher-level Java representation. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @param v the raw value, or {@code null} to set the CQL value {@code NULL}. For performance + * reasons, this is the actual instance used internally. If pass in a buffer that you're going + * to modify elsewhere in your application, make sure to {@link ByteBuffer#duplicate() + * duplicate} it beforehand. If you change the buffer's index or its contents in any way, + * further usage of this data will have unpredictable results. + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBytesUnsafe(@NonNull CqlIdentifier id, @Nullable ByteBuffer v) { + return setBytesUnsafe(firstIndexOf(id), v); + } + + @NonNull + @Override + default DataType getType(@NonNull CqlIdentifier id) { + return getType(firstIndexOf(id)); + } + + /** + * Sets the value for the first occurrence of {@code id} to CQL {@code NULL}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setToNull(@NonNull CqlIdentifier id) { + return setToNull(firstIndexOf(id)); + } + + /** + * Sets the value for the first occurrence of {@code id}, using the given codec for the + * conversion. + * + *

This method completely bypasses the {@link #codecRegistry()}, and forces the driver to use + * the given codec instead. This can be useful if the codec would collide with a previously + * registered one, or if you want to use the codec just once without registering it. + * + *

It is the caller's responsibility to ensure that the given codec is appropriate for the + * conversion. Failing to do so will result in errors at runtime. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT set( + @NonNull CqlIdentifier id, @Nullable ValueT v, @NonNull TypeCodec codec) { + return set(firstIndexOf(id), v, codec); + } + + /** + * Sets the value for the first occurrence of {@code id}, converting it to the given Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

This variant is for generic Java types. If the target type is not generic, use {@link + * #set(int, Object, Class)} instead, which may perform slightly better. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @NonNull + @CheckReturnValue + default SelfT set( + @NonNull CqlIdentifier id, @Nullable ValueT v, @NonNull GenericType targetType) { + return set(firstIndexOf(id), v, targetType); + } + + /** + * Returns the value for the first occurrence of {@code id}, converting it to the given Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

If the target type is generic, use {@link #set(int, Object, GenericType)} instead. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @NonNull + @CheckReturnValue + default SelfT set( + @NonNull CqlIdentifier id, @Nullable ValueT v, @NonNull Class targetClass) { + return set(firstIndexOf(id), v, targetClass); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java primitive boolean. + * + *

By default, this works with CQL type {@code boolean}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Boolean.class)}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBoolean(@NonNull CqlIdentifier id, boolean v) { + return setBoolean(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java primitive byte. + * + *

By default, this works with CQL type {@code tinyint}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Boolean.class)}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setByte(@NonNull CqlIdentifier id, byte v) { + return setByte(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java primitive double. + * + *

By default, this works with CQL type {@code double}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Double.class)}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setDouble(@NonNull CqlIdentifier id, double v) { + return setDouble(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java primitive float. + * + *

By default, this works with CQL type {@code float}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Float.class)}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setFloat(@NonNull CqlIdentifier id, float v) { + return setFloat(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java primitive integer. + * + *

By default, this works with CQL type {@code int}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Integer.class)}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setInt(@NonNull CqlIdentifier id, int v) { + return setInt(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java primitive long. + * + *

By default, this works with CQL types {@code bigint} and {@code counter}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Long.class)}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setLong(@NonNull CqlIdentifier id, long v) { + return setLong(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java primitive short. + * + *

By default, this works with CQL type {@code smallint}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Short.class)}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setShort(@NonNull CqlIdentifier id, short v) { + return setShort(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java instant. + * + *

By default, this works with CQL type {@code timestamp}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setInstant(@NonNull CqlIdentifier id, @Nullable Instant v) { + return setInstant(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java local date. + * + *

By default, this works with CQL type {@code date}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setLocalDate(@NonNull CqlIdentifier id, @Nullable LocalDate v) { + return setLocalDate(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java local time. + * + *

By default, this works with CQL type {@code time}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setLocalTime(@NonNull CqlIdentifier id, @Nullable LocalTime v) { + return setLocalTime(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java byte buffer. + * + *

By default, this works with CQL type {@code blob}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setByteBuffer(@NonNull CqlIdentifier id, @Nullable ByteBuffer v) { + return setByteBuffer(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java string. + * + *

By default, this works with CQL types {@code text}, {@code varchar} and {@code ascii}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setString(@NonNull CqlIdentifier id, @Nullable String v) { + return setString(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java big integer. + * + *

By default, this works with CQL type {@code varint}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBigInteger(@NonNull CqlIdentifier id, @Nullable BigInteger v) { + return setBigInteger(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java big decimal. + * + *

By default, this works with CQL type {@code decimal}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBigDecimal(@NonNull CqlIdentifier id, @Nullable BigDecimal v) { + return setBigDecimal(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java UUID. + * + *

By default, this works with CQL types {@code uuid} and {@code timeuuid}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setUuid(@NonNull CqlIdentifier id, @Nullable UUID v) { + return setUuid(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java IP address. + * + *

By default, this works with CQL type {@code inet}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setInetAddress(@NonNull CqlIdentifier id, @Nullable InetAddress v) { + return setInetAddress(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided duration. + * + *

By default, this works with CQL type {@code duration}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setCqlDuration(@NonNull CqlIdentifier id, @Nullable CqlDuration v) { + return setCqlDuration(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided token. + * + *

This works with the CQL type matching the partitioner in use for this cluster: {@code + * bigint} for {@code Murmur3Partitioner}, {@code blob} for {@code ByteOrderedPartitioner}, and + * {@code varint} for {@code RandomPartitioner}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setToken(@NonNull CqlIdentifier id, @NonNull Token v) { + return setToken(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java list. + * + *

By default, this works with CQL type {@code list}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex list types, use {@link #set(int, Object, GenericType)}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setList( + @NonNull CqlIdentifier id, + @Nullable List v, + @NonNull Class elementsClass) { + return setList(firstIndexOf(id), v, elementsClass); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java set. + * + *

By default, this works with CQL type {@code set}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex set types, use {@link #set(int, Object, GenericType)}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setSet( + @NonNull CqlIdentifier id, + @Nullable Set v, + @NonNull Class elementsClass) { + return setSet(firstIndexOf(id), v, elementsClass); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided Java map. + * + *

By default, this works with CQL type {@code map}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex map types, use {@link #set(int, Object, GenericType)}. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setMap( + @NonNull CqlIdentifier id, + @Nullable Map v, + @NonNull Class keyClass, + @NonNull Class valueClass) { + return setMap(firstIndexOf(id), v, keyClass, valueClass); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided user defined type value. + * + *

By default, this works with CQL user-defined types. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setUdtValue(@NonNull CqlIdentifier id, @Nullable UdtValue v) { + return setUdtValue(firstIndexOf(id), v); + } + + /** + * Sets the value for the first occurrence of {@code id} to the provided tuple value. + * + *

By default, this works with CQL tuples. + * + *

If you want to avoid the overhead of building a {@code CqlIdentifier}, use the variant of + * this method that takes a string argument. + * + * @throws IllegalArgumentException if the id is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setTupleValue(@NonNull CqlIdentifier id, @Nullable TupleValue v) { + return setTupleValue(firstIndexOf(id), v); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/SettableByIndex.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/SettableByIndex.java new file mode 100644 index 00000000000..2ff700cc3fa --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/SettableByIndex.java @@ -0,0 +1,512 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveBooleanCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveByteCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveDoubleCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveFloatCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveIntCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveLongCodec; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveShortCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.metadata.token.ByteOrderedToken; +import com.datastax.oss.driver.internal.core.metadata.token.Murmur3Token; +import com.datastax.oss.driver.internal.core.metadata.token.RandomToken; +import edu.umd.cs.findbugs.annotations.CheckReturnValue; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** A data structure that provides methods to set its values via an integer index. */ +public interface SettableByIndex> extends AccessibleByIndex { + + /** + * Sets the raw binary representation of the {@code i}th value. + * + *

This is primarily for internal use; you'll likely want to use one of the typed setters + * instead, to pass a higher-level Java representation. + * + * @param v the raw value, or {@code null} to set the CQL value {@code NULL}. For performance + * reasons, this is the actual instance used internally. If pass in a buffer that you're going + * to modify elsewhere in your application, make sure to {@link ByteBuffer#duplicate() + * duplicate} it beforehand. If you change the buffer's index or its contents in any way, + * further usage of this data will have unpredictable results. + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + SelfT setBytesUnsafe(int i, @Nullable ByteBuffer v); + + /** + * Sets the {@code i}th value to CQL {@code NULL}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setToNull(int i) { + return setBytesUnsafe(i, null); + } + + /** + * Sets the {@code i}th value, using the given codec for the conversion. + * + *

This method completely bypasses the {@link #codecRegistry()}, and forces the driver to use + * the given codec instead. This can be useful if the codec would collide with a previously + * registered one, or if you want to use the codec just once without registering it. + * + *

It is the caller's responsibility to ensure that the given codec is appropriate for the + * conversion. Failing to do so will result in errors at runtime. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT set(int i, @Nullable ValueT v, @NonNull TypeCodec codec) { + return setBytesUnsafe(i, codec.encode(v, protocolVersion())); + } + + /** + * Sets the {@code i}th value, converting it to the given Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

This variant is for generic Java types. If the target type is not generic, use {@link + * #set(int, Object, Class)} instead, which may perform slightly better. + * + * @throws IndexOutOfBoundsException if the index is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @NonNull + @CheckReturnValue + default SelfT set(int i, @Nullable ValueT v, @NonNull GenericType targetType) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, targetType); + return set(i, v, codec); + } + + /** + * Returns the {@code i}th value, converting it to the given Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

If the target type is generic, use {@link #set(int, Object, GenericType)} instead. + * + * @throws IndexOutOfBoundsException if the index is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @NonNull + @CheckReturnValue + default SelfT set(int i, @Nullable ValueT v, @NonNull Class targetClass) { + // This is duplicated from the GenericType variant, because we want to give the codec registry + // a chance to process the unwrapped class directly, if it can do so in a more efficient way. + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, targetClass); + return set(i, v, codec); + } + + /** + * Sets the {@code i}th value to the provided Java primitive boolean. + * + *

By default, this works with CQL type {@code boolean}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Boolean.class)}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBoolean(int i, boolean v) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Boolean.class); + return (codec instanceof PrimitiveBooleanCodec) + ? setBytesUnsafe(i, ((PrimitiveBooleanCodec) codec).encodePrimitive(v, protocolVersion())) + : set(i, v, codec); + } + + /** + * Sets the {@code i}th value to the provided Java primitive byte. + * + *

By default, this works with CQL type {@code tinyint}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Boolean.class)}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setByte(int i, byte v) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Byte.class); + return (codec instanceof PrimitiveByteCodec) + ? setBytesUnsafe(i, ((PrimitiveByteCodec) codec).encodePrimitive(v, protocolVersion())) + : set(i, v, codec); + } + + /** + * Sets the {@code i}th value to the provided Java primitive double. + * + *

By default, this works with CQL type {@code double}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Double.class)}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setDouble(int i, double v) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Double.class); + return (codec instanceof PrimitiveDoubleCodec) + ? setBytesUnsafe(i, ((PrimitiveDoubleCodec) codec).encodePrimitive(v, protocolVersion())) + : set(i, v, codec); + } + + /** + * Sets the {@code i}th value to the provided Java primitive float. + * + *

By default, this works with CQL type {@code float}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Float.class)}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setFloat(int i, float v) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Float.class); + return (codec instanceof PrimitiveFloatCodec) + ? setBytesUnsafe(i, ((PrimitiveFloatCodec) codec).encodePrimitive(v, protocolVersion())) + : set(i, v, codec); + } + + /** + * Sets the {@code i}th value to the provided Java primitive integer. + * + *

By default, this works with CQL type {@code int}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Integer.class)}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setInt(int i, int v) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Integer.class); + return (codec instanceof PrimitiveIntCodec) + ? setBytesUnsafe(i, ((PrimitiveIntCodec) codec).encodePrimitive(v, protocolVersion())) + : set(i, v, codec); + } + + /** + * Sets the {@code i}th value to the provided Java primitive long. + * + *

By default, this works with CQL types {@code bigint} and {@code counter}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Long.class)}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setLong(int i, long v) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Long.class); + return (codec instanceof PrimitiveLongCodec) + ? setBytesUnsafe(i, ((PrimitiveLongCodec) codec).encodePrimitive(v, protocolVersion())) + : set(i, v, codec); + } + + /** + * Sets the {@code i}th value to the provided Java primitive short. + * + *

By default, this works with CQL type {@code smallint}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Short.class)}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setShort(int i, short v) { + DataType cqlType = getType(i); + TypeCodec codec = codecRegistry().codecFor(cqlType, Short.class); + return (codec instanceof PrimitiveShortCodec) + ? setBytesUnsafe(i, ((PrimitiveShortCodec) codec).encodePrimitive(v, protocolVersion())) + : set(i, v, codec); + } + + /** + * Sets the {@code i}th value to the provided Java instant. + * + *

By default, this works with CQL type {@code timestamp}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setInstant(int i, @Nullable Instant v) { + return set(i, v, Instant.class); + } + + /** + * Sets the {@code i}th value to the provided Java local date. + * + *

By default, this works with CQL type {@code date}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setLocalDate(int i, @Nullable LocalDate v) { + return set(i, v, LocalDate.class); + } + + /** + * Sets the {@code i}th value to the provided Java local time. + * + *

By default, this works with CQL type {@code time}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setLocalTime(int i, @Nullable LocalTime v) { + return set(i, v, LocalTime.class); + } + + /** + * Sets the {@code i}th value to the provided Java byte buffer. + * + *

By default, this works with CQL type {@code blob}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setByteBuffer(int i, @Nullable ByteBuffer v) { + return set(i, v, ByteBuffer.class); + } + + /** + * Sets the {@code i}th value to the provided Java string. + * + *

By default, this works with CQL types {@code text}, {@code varchar} and {@code ascii}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setString(int i, @Nullable String v) { + return set(i, v, String.class); + } + + /** + * Sets the {@code i}th value to the provided Java big integer. + * + *

By default, this works with CQL type {@code varint}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBigInteger(int i, @Nullable BigInteger v) { + return set(i, v, BigInteger.class); + } + + /** + * Sets the {@code i}th value to the provided Java big decimal. + * + *

By default, this works with CQL type {@code decimal}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBigDecimal(int i, @Nullable BigDecimal v) { + return set(i, v, BigDecimal.class); + } + + /** + * Sets the {@code i}th value to the provided Java UUID. + * + *

By default, this works with CQL types {@code uuid} and {@code timeuuid}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setUuid(int i, @Nullable UUID v) { + return set(i, v, UUID.class); + } + + /** + * Sets the {@code i}th value to the provided Java IP address. + * + *

By default, this works with CQL type {@code inet}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setInetAddress(int i, @Nullable InetAddress v) { + return set(i, v, InetAddress.class); + } + + /** + * Sets the {@code i}th value to the provided duration. + * + *

By default, this works with CQL type {@code duration}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setCqlDuration(int i, @Nullable CqlDuration v) { + return set(i, v, CqlDuration.class); + } + + /** + * Sets the {@code i}th value to the provided token. + * + *

This works with the CQL type matching the partitioner in use for this cluster: {@code + * bigint} for {@code Murmur3Partitioner}, {@code blob} for {@code ByteOrderedPartitioner}, and + * {@code varint} for {@code RandomPartitioner}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setToken(int i, @NonNull Token v) { + // Simply enumerate all known implementations. This goes against the concept of TokenFactory, + // but injecting the factory here is too much of a hassle. + // The only issue is if someone uses a custom partitioner, but this is highly unlikely, and even + // then they can set the value manually as a workaround. + if (v instanceof Murmur3Token) { + return setLong(i, ((Murmur3Token) v).getValue()); + } else if (v instanceof ByteOrderedToken) { + return setByteBuffer(i, ((ByteOrderedToken) v).getValue()); + } else if (v instanceof RandomToken) { + return setBigInteger(i, ((RandomToken) v).getValue()); + } else { + throw new IllegalArgumentException("Unsupported token type " + v.getClass()); + } + } + + /** + * Sets the {@code i}th value to the provided Java list. + * + *

By default, this works with CQL type {@code list}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex list types, use {@link #set(int, Object, GenericType)}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setList( + int i, @Nullable List v, @NonNull Class elementsClass) { + return set(i, v, GenericType.listOf(elementsClass)); + } + + /** + * Sets the {@code i}th value to the provided Java set. + * + *

By default, this works with CQL type {@code set}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex set types, use {@link #set(int, Object, GenericType)}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setSet( + int i, @Nullable Set v, @NonNull Class elementsClass) { + return set(i, v, GenericType.setOf(elementsClass)); + } + + /** + * Sets the {@code i}th value to the provided Java map. + * + *

By default, this works with CQL type {@code map}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex map types, use {@link #set(int, Object, GenericType)}. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setMap( + int i, + @Nullable Map v, + @NonNull Class keyClass, + @NonNull Class valueClass) { + return set(i, v, GenericType.mapOf(keyClass, valueClass)); + } + + /** + * Sets the {@code i}th value to the provided user defined type value. + * + *

By default, this works with CQL user-defined types. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setUdtValue(int i, @Nullable UdtValue v) { + return set(i, v, UdtValue.class); + } + + /** + * Sets the {@code i}th value to the provided tuple value. + * + *

By default, this works with CQL tuples. + * + * @throws IndexOutOfBoundsException if the index is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setTupleValue(int i, @Nullable TupleValue v) { + return set(i, v, TupleValue.class); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/SettableByName.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/SettableByName.java new file mode 100644 index 00000000000..0ebd95b22cc --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/SettableByName.java @@ -0,0 +1,555 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.CheckReturnValue; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** A data structure that provides methods to set its values via a name. */ +public interface SettableByName> + extends SettableByIndex, AccessibleByName { + + /** + * Sets the raw binary representation of the value for the first occurrence of {@code name}. + * + *

This is primarily for internal use; you'll likely want to use one of the typed setters + * instead, to pass a higher-level Java representation. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @param v the raw value, or {@code null} to set the CQL value {@code NULL}. For performance + * reasons, this is the actual instance used internally. If pass in a buffer that you're going + * to modify elsewhere in your application, make sure to {@link ByteBuffer#duplicate() + * duplicate} it beforehand. If you change the buffer's index or its contents in any way, + * further usage of this data will have unpredictable results. + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBytesUnsafe(@NonNull String name, @Nullable ByteBuffer v) { + return setBytesUnsafe(firstIndexOf(name), v); + } + + @NonNull + @Override + default DataType getType(@NonNull String name) { + return getType(firstIndexOf(name)); + } + + /** + * Sets the value for the first occurrence of {@code name} to CQL {@code NULL}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setToNull(@NonNull String name) { + return setToNull(firstIndexOf(name)); + } + + /** + * Sets the value for the first occurrence of {@code name}, using the given codec for the + * conversion. + * + *

This method completely bypasses the {@link #codecRegistry()}, and forces the driver to use + * the given codec instead. This can be useful if the codec would collide with a previously + * registered one, or if you want to use the codec just once without registering it. + * + *

It is the caller's responsibility to ensure that the given codec is appropriate for the + * conversion. Failing to do so will result in errors at runtime. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT set( + @NonNull String name, @Nullable ValueT v, @NonNull TypeCodec codec) { + return set(firstIndexOf(name), v, codec); + } + + /** + * Sets the value for the first occurrence of {@code name}, converting it to the given Java type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

This variant is for generic Java types. If the target type is not generic, use {@link + * #set(int, Object, Class)} instead, which may perform slightly better. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @NonNull + @CheckReturnValue + default SelfT set( + @NonNull String name, @Nullable ValueT v, @NonNull GenericType targetType) { + return set(firstIndexOf(name), v, targetType); + } + + /** + * Returns the value for the first occurrence of {@code name}, converting it to the given Java + * type. + * + *

The {@link #codecRegistry()} will be used to look up a codec to handle the conversion. + * + *

If the target type is generic, use {@link #set(int, Object, GenericType)} instead. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + * @throws CodecNotFoundException if no codec can perform the conversion. + */ + @NonNull + @CheckReturnValue + default SelfT set( + @NonNull String name, @Nullable ValueT v, @NonNull Class targetClass) { + return set(firstIndexOf(name), v, targetClass); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java primitive boolean. + * + *

By default, this works with CQL type {@code boolean}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Boolean.class)}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBoolean(@NonNull String name, boolean v) { + return setBoolean(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java primitive byte. + * + *

By default, this works with CQL type {@code tinyint}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Boolean.class)}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setByte(@NonNull String name, byte v) { + return setByte(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java primitive double. + * + *

By default, this works with CQL type {@code double}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Double.class)}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setDouble(@NonNull String name, double v) { + return setDouble(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java primitive float. + * + *

By default, this works with CQL type {@code float}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Float.class)}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setFloat(@NonNull String name, float v) { + return setFloat(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java primitive integer. + * + *

By default, this works with CQL type {@code int}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Integer.class)}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setInt(@NonNull String name, int v) { + return setInt(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java primitive long. + * + *

By default, this works with CQL types {@code bigint} and {@code counter}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Long.class)}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setLong(@NonNull String name, long v) { + return setLong(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java primitive short. + * + *

By default, this works with CQL type {@code smallint}. + * + *

To set the value to CQL {@code NULL}, use {@link #setToNull(int)}, or {@code set(i, v, + * Short.class)}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setShort(@NonNull String name, short v) { + return setShort(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java instant. + * + *

By default, this works with CQL type {@code timestamp}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setInstant(@NonNull String name, @Nullable Instant v) { + return setInstant(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java local date. + * + *

By default, this works with CQL type {@code date}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setLocalDate(@NonNull String name, @Nullable LocalDate v) { + return setLocalDate(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java local time. + * + *

By default, this works with CQL type {@code time}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setLocalTime(@NonNull String name, @Nullable LocalTime v) { + return setLocalTime(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java byte buffer. + * + *

By default, this works with CQL type {@code blob}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setByteBuffer(@NonNull String name, @Nullable ByteBuffer v) { + return setByteBuffer(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java string. + * + *

By default, this works with CQL types {@code text}, {@code varchar} and {@code ascii}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setString(@NonNull String name, @Nullable String v) { + return setString(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java big integer. + * + *

By default, this works with CQL type {@code varint}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBigInteger(@NonNull String name, @Nullable BigInteger v) { + return setBigInteger(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java big decimal. + * + *

By default, this works with CQL type {@code decimal}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setBigDecimal(@NonNull String name, @Nullable BigDecimal v) { + return setBigDecimal(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java UUID. + * + *

By default, this works with CQL types {@code uuid} and {@code timeuuid}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setUuid(@NonNull String name, @Nullable UUID v) { + return setUuid(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java IP address. + * + *

By default, this works with CQL type {@code inet}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setInetAddress(@NonNull String name, @Nullable InetAddress v) { + return setInetAddress(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided duration. + * + *

By default, this works with CQL type {@code duration}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setCqlDuration(@NonNull String name, @Nullable CqlDuration v) { + return setCqlDuration(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided token. + * + *

This works with the CQL type matching the partitioner in use for this cluster: {@code + * bigint} for {@code Murmur3Partitioner}, {@code blob} for {@code ByteOrderedPartitioner}, and + * {@code varint} for {@code RandomPartitioner}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setToken(@NonNull String name, @NonNull Token v) { + return setToken(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java list. + * + *

By default, this works with CQL type {@code list}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex list types, use {@link #set(int, Object, GenericType)}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setList( + @NonNull String name, @Nullable List v, @NonNull Class elementsClass) { + return setList(firstIndexOf(name), v, elementsClass); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java set. + * + *

By default, this works with CQL type {@code set}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex set types, use {@link #set(int, Object, GenericType)}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setSet( + @NonNull String name, @Nullable Set v, @NonNull Class elementsClass) { + return setSet(firstIndexOf(name), v, elementsClass); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided Java map. + * + *

By default, this works with CQL type {@code map}. + * + *

This method is provided for convenience when the element type is a non-generic type. For + * more complex map types, use {@link #set(int, Object, GenericType)}. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setMap( + @NonNull String name, + @Nullable Map v, + @NonNull Class keyClass, + @NonNull Class valueClass) { + return setMap(firstIndexOf(name), v, keyClass, valueClass); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided user defined type + * value. + * + *

By default, this works with CQL user-defined types. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setUdtValue(@NonNull String name, @Nullable UdtValue v) { + return setUdtValue(firstIndexOf(name), v); + } + + /** + * Sets the value for the first occurrence of {@code name} to the provided tuple value. + * + *

By default, this works with CQL tuples. + * + *

This method deals with case sensitivity in the way explained in the documentation of {@link + * AccessibleByName}. + * + * @throws IllegalArgumentException if the name is invalid. + */ + @NonNull + @CheckReturnValue + default SelfT setTupleValue(@NonNull String name, @Nullable TupleValue v) { + return setTupleValue(firstIndexOf(name), v); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/TupleValue.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/TupleValue.java new file mode 100644 index 00000000000..5937bc0517c --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/TupleValue.java @@ -0,0 +1,37 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.detach.Detachable; +import com.datastax.oss.driver.api.core.type.TupleType; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Driver-side representation of a CQL {@code tuple} value. + * + *

It is an ordered set of anonymous, typed fields. + * + *

A tuple value is attached if and only if its type is attached (see {@link Detachable}). + * + *

The default implementation returned by the driver is immutable and serializable. If you write + * your own implementation, it should at least be thread-safe; serializability is not mandatory, but + * recommended for use with some 3rd-party tools like Apache Spark ™. + */ +public interface TupleValue extends GettableByIndex, SettableByIndex { + + @NonNull + TupleType getType(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/data/UdtValue.java b/core/src/main/java/com/datastax/oss/driver/api/core/data/UdtValue.java new file mode 100644 index 00000000000..bfdebdfd7fa --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/data/UdtValue.java @@ -0,0 +1,38 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import com.datastax.oss.driver.api.core.detach.Detachable; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Driver-side representation of an instance of a CQL user defined type. + * + *

It is an ordered set of named, typed fields. + * + *

A tuple value is attached if and only if its type is attached (see {@link Detachable}). + * + *

The default implementation returned by the driver is immutable and serializable. If you write + * your own implementation, it should at least be thread-safe; serializability is not mandatory, but + * recommended for use with some 3rd-party tools like Apache Spark ™. + */ +public interface UdtValue + extends GettableById, GettableByName, SettableById, SettableByName { + + @NonNull + UserDefinedType getType(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/detach/AttachmentPoint.java b/core/src/main/java/com/datastax/oss/driver/api/core/detach/AttachmentPoint.java new file mode 100644 index 00000000000..930a72a35fd --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/detach/AttachmentPoint.java @@ -0,0 +1,44 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.detach; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** @see Detachable */ +public interface AttachmentPoint { + AttachmentPoint NONE = + new AttachmentPoint() { + @NonNull + @Override + public ProtocolVersion getProtocolVersion() { + return ProtocolVersion.DEFAULT; + } + + @NonNull + @Override + public CodecRegistry getCodecRegistry() { + return CodecRegistry.DEFAULT; + } + }; + + @NonNull + ProtocolVersion getProtocolVersion(); + + @NonNull + CodecRegistry getCodecRegistry(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/detach/Detachable.java b/core/src/main/java/com/datastax/oss/driver/api/core/detach/Detachable.java new file mode 100644 index 00000000000..73d1f804fac --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/detach/Detachable.java @@ -0,0 +1,55 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.detach; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.Data; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Defines the contract of an object that can be detached and reattached to a driver instance. + * + *

The driver's {@link Data data structure} types (such as rows, tuples and UDT values) store + * their data as byte buffers, and only decode it on demand, when the end user accesses a particular + * column or field. + * + *

Decoding requires a {@link ProtocolVersion} (because the encoded format might change across + * versions), and a {@link CodecRegistry} (because the user might ask us to decode to a custom + * type). + * + *

    + *
  • When a data container was obtained from a driver instance (for example, reading a row from + * a result set, or reading a value from a UDT column), it is attached: its protocol + * version and registry are those of the driver. + *
  • When it is created manually by the user (for example, creating an instance from a manually + * created {@link TupleType}), it is detached: it uses {@link + * ProtocolVersion#DEFAULT} and {@link CodecRegistry#DEFAULT}. + *
+ * + * The only way an attached object can become detached is if it is serialized and deserialized + * (referring to Java serialization). + * + *

A detached object can be reattached to a driver instance. This is done automatically if you + * pass the object to one of the driver methods, for example if you use a manually created tuple as + * a query parameter. + */ +public interface Detachable { + boolean isDetached(); + + void attach(@NonNull AttachmentPoint attachmentPoint); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/loadbalancing/LoadBalancingPolicy.java b/core/src/main/java/com/datastax/oss/driver/api/core/loadbalancing/LoadBalancingPolicy.java new file mode 100644 index 00000000000..425e11c0c5a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/loadbalancing/LoadBalancingPolicy.java @@ -0,0 +1,109 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.loadbalancing; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import java.util.Queue; +import java.util.UUID; + +/** Decides which Cassandra nodes to contact for each query. */ +public interface LoadBalancingPolicy extends AutoCloseable { + + /** + * Initializes this policy with the nodes discovered during driver initialization. + * + *

This method is guaranteed to be called exactly once per instance, and before any other + * method in this class. At this point, the driver has successfully connected to one of the + * contact points, and performed a first refresh of topology information (by default, the contents + * of {@code system.peers}), to discover other nodes in the cluster. + * + *

This method must call {@link DistanceReporter#setDistance(Node, NodeDistance) + * distanceReporter.setDistance} for each provided node (otherwise that node will stay at distance + * {@link NodeDistance#IGNORED IGNORED}, and the driver won't open connections to it). Note that + * the node's {@link Node#getState() state} can be either {@link NodeState#UP UP} (for the + * successful contact point), {@link NodeState#DOWN DOWN} (for contact points that were tried + * unsuccessfully), or {@link NodeState#UNKNOWN UNKNOWN} (for contact points that weren't tried, + * or any other node discovered from the topology refresh). Node states may be updated + * concurrently while this method executes, but if so this policy will get notified after this + * method has returned, through other methods such as {@link #onUp(Node)} or {@link + * #onDown(Node)}. + * + * @param nodes all the nodes that are known to exist in the cluster (regardless of their state) + * at the time of invocation. + * @param distanceReporter an object that will be used by the policy to signal distance changes. + * Implementations will typically store a this in a field, since new nodes may get {@link + * #onAdd(Node) added} later and will need to have their distance set (or the policy might + * change distances dynamically over time). + */ + void init(@NonNull Map nodes, @NonNull DistanceReporter distanceReporter); + + /** + * Returns the coordinators to use for a new query. + * + *

Each new query will call this method, and try the returned nodes sequentially. + * + * @param request the request that is being routed. Note that this can be null for some internal + * uses. + * @param session the session that is executing the request. Note that this can be null for some + * internal uses. + * @return the list of coordinators to try. This must be a concurrent queue; {@link + * java.util.concurrent.ConcurrentLinkedQueue} is a good choice. + */ + @NonNull + Queue newQueryPlan(@Nullable Request request, @Nullable Session session); + + /** + * Called when a node is added to the cluster. + * + *

The new node will be at distance {@link NodeDistance#IGNORED IGNORED}, and have the state + * {@link NodeState#UNKNOWN UNKNOWN}. + * + *

If this method assigns an active distance to the node, the driver will try to create a + * connection pool to it (resulting in a state change to {@link #onUp(Node) UP} or {@link + * #onDown(Node) DOWN} depending on the outcome). + * + *

If it leaves it at distance {@link NodeDistance#IGNORED IGNORED}, the driver won't attempt + * any connection. The node state will remain unknown, but might be updated later if a topology + * event is received from the cluster. + * + * @see #init(Map, DistanceReporter) + */ + void onAdd(@NonNull Node node); + + /** Called when a node is determined to be up. */ + void onUp(@NonNull Node node); + + /** Called when a node is determined to be down. */ + void onDown(@NonNull Node node); + + /** Called when a node is removed from the cluster. */ + void onRemove(@NonNull Node node); + + /** Called when the cluster that this policy is associated with closes. */ + @Override + void close(); + + /** An object that the policy uses to signal decisions it makes about node distances. */ + interface DistanceReporter { + void setDistance(@NonNull Node node, @NonNull NodeDistance distance); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/loadbalancing/NodeDistance.java b/core/src/main/java/com/datastax/oss/driver/api/core/loadbalancing/NodeDistance.java new file mode 100644 index 00000000000..0bff43d6a46 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/loadbalancing/NodeDistance.java @@ -0,0 +1,42 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.loadbalancing; +/** + * Determines how the driver will manage connections to a Cassandra node. + * + *

The distance is assigned by a {@link LoadBalancingPolicy}. + */ +public enum NodeDistance { + /** + * An "active" distance that, indicates that the driver should maintain connections to the node; + * it also marks it as "preferred", meaning that the number or capacity of the connections may be + * higher, and that the node may also have priority for some tasks (for example, being chosen as + * the control host). + */ + LOCAL, + /** + * An "active" distance that, indicates that the driver should maintain connections to the node; + * it also marks it as "less preferred", meaning that the number or capacity of the connections + * may be lower, and that other nodes may have a higher priority for some tasks (for example, + * being chosen as the control host). + */ + REMOTE, + /** + * An "inactive" distance, that indicates that the driver will not open any connection to the + * node. + */ + IGNORED, +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/EndPoint.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/EndPoint.java new file mode 100644 index 00000000000..d09253e099a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/EndPoint.java @@ -0,0 +1,47 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +/** + * Encapsulates the information needed to open connections to a node. + * + *

By default, the driver assumes plain TCP connections, and this is just a wrapper around an + * {@link InetSocketAddress}. However, more complex deployment scenarios might use a custom + * implementation that contains additional information; for example, if the nodes are accessed + * through a proxy with SNI routing, an SNI server name is needed in addition to the proxy address. + */ +public interface EndPoint { + + /** + * Resolves this instance to a socket address. + * + *

This will be called each time the driver opens a new connection to the node. The returned + * address cannot be null. + */ + SocketAddress resolve(); + + /** + * Returns an alternate string representation for use in node-level metric names. + * + *

Because metrics names are path-like, dot-separated strings, raw IP addresses don't make very + * good identifiers. So this method will typically replace the dots by another character, for + * example {@code 127_0_0_1_9042}. + */ + String asMetricPrefix(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/Metadata.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/Metadata.java new file mode 100644 index 00000000000..a996d8a1eaf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/Metadata.java @@ -0,0 +1,117 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.api.core.session.Session; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** + * The metadata of the Cassandra cluster that this driver instance is connected to. + * + *

Updates to this object are guaranteed to be atomic: the node list, schema, and token metadata + * are immutable, and will always be consistent for a given metadata instance. The node instances + * are the only mutable objects in the hierarchy, and some of their fields will be modified + * dynamically (in particular the node state). + * + * @see Session#getMetadata() + */ +public interface Metadata { + /** + * The nodes known to the driver, indexed by their unique identifier ({@code host_id} in {@code + * system.local}/{@code system.peers}). This might include nodes that are currently viewed as + * down, or ignored by the load balancing policy. + */ + @NonNull + Map getNodes(); + + /** + * Finds the node with the given {@linkplain Node#getEndPoint() connection information}, if it + * exists. + * + *

Note that this method performs a linear search of {@link #getNodes()}. + */ + @NonNull + default Optional findNode(@NonNull EndPoint endPoint) { + for (Node node : getNodes().values()) { + if (node.getEndPoint().equals(endPoint)) { + return Optional.of(node); + } + } + return Optional.empty(); + } + + /** + * Finds the node with the given untranslated {@linkplain Node#getBroadcastRpcAddress() + * broadcast RPC address}, if it exists. + * + *

Note that this method performs a linear search of {@link #getNodes()}. + */ + @NonNull + default Optional findNode(@NonNull InetSocketAddress broadcastRpcAddress) { + for (Node node : getNodes().values()) { + Optional o = node.getBroadcastRpcAddress(); + if (o.isPresent() && o.get().equals(broadcastRpcAddress)) { + return Optional.of(node); + } + } + return Optional.empty(); + } + + /** + * The keyspaces defined in this cluster. + * + *

Note that schema metadata can be disabled or restricted to a subset of keyspaces, therefore + * this map might be empty or incomplete. + * + * @see DefaultDriverOption#METADATA_SCHEMA_ENABLED + * @see Session#setSchemaMetadataEnabled(Boolean) + * @see DefaultDriverOption#METADATA_SCHEMA_REFRESHED_KEYSPACES + */ + @NonNull + Map getKeyspaces(); + + @NonNull + default Optional getKeyspace(@NonNull CqlIdentifier keyspaceId) { + return Optional.ofNullable(getKeyspaces().get(keyspaceId)); + } + + /** + * Shortcut for {@link #getKeyspace(CqlIdentifier) + * getKeyspace(CqlIdentifier.fromCql(keyspaceName))}. + */ + @NonNull + default Optional getKeyspace(@NonNull String keyspaceName) { + return getKeyspace(CqlIdentifier.fromCql(keyspaceName)); + } + + /** + * The token map for this cluster. + * + *

Note that this property might be absent if token metadata was disabled, or if there was a + * runtime error while computing the map (this would generate a warning log). + * + * @see DefaultDriverOption#METADATA_TOKEN_MAP_ENABLED + */ + @NonNull + Optional getTokenMap(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/Node.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/Node.java new file mode 100644 index 00000000000..019e2955848 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/Node.java @@ -0,0 +1,174 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata; + +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** + * Metadata about a Cassandra node in the cluster. + * + *

This object is mutable, all of its properties may be updated at runtime to reflect the latest + * state of the node. + */ +public interface Node { + + /** The information that the driver uses to connect to the node. */ + @NonNull + EndPoint getEndPoint(); + + /** + * The node's broadcast RPC address. + * + *

This is the address that the node expects clients to connect to, as reported in {@code + * system.peers.rpc_address} (Cassandra 3) or {@code system.peers_v2.native_address/native_port} + * (Cassandra 4+). However, it might not be what the driver uses directly, if the node is accessed + * through a proxy. + * + *

This may not be known at all times. In particular, some Cassandra versions (less than + * 2.0.16, 2.1.6 or 2.2.0-rc1) don't store it in the {@code system.local} table, so this will be + * unknown for the control node, until the control connection reconnects to another node. + * + * @see CASSANDRA-9436 (where the + * information was added to system.local) + */ + @NonNull + Optional getBroadcastRpcAddress(); + + /** + * The node's broadcast address. That is, the address that other nodes use to communicate with + * that node. This is also the value of the {@code peer} column in {@code system.peers}. If the + * port is set to 0 it is unknown. + * + *

This may not be known at all times. In particular, some Cassandra versions (less than + * 2.0.16, 2.1.6 or 2.2.0-rc1) don't store it in the {@code system.local} table, so this will be + * unknown for the control node, until the control connection reconnects to another node. + * + * @see CASSANDRA-9436 (where the + * information was added to system.local) + */ + @NonNull + Optional getBroadcastAddress(); + + /** + * The node's listen address. That is, the address that the Cassandra process binds to. If the + * port is set to 0 it is unknown. + * + *

This may not be know at all times. In particular, current Cassandra versions (3.10) only + * store it in {@code system.local}, so this will be known only for the control node. + */ + @NonNull + Optional getListenAddress(); + + /** + * The datacenter that this node belongs to (according to the server-side snitch). + * + *

This should be non-null in a healthy deployment, but the driver will still function, and + * report {@code null} here, if the server metadata was corrupted. + */ + @Nullable + String getDatacenter(); + + /** + * The rack that this node belongs to (according to the server-side snitch). + * + *

This should be non-null in a healthy deployment, but the driver will still function, and + * report {@code null} here, if the server metadata was corrupted. + */ + @Nullable + String getRack(); + + /** + * The Cassandra version of the server. + * + *

This should be non-null in a healthy deployment, but the driver will still function, and + * report {@code null} here, if the server metadata was corrupted or the reported version could + * not be parsed. + */ + @Nullable + Version getCassandraVersion(); + + /** + * An additional map of free-form properties. + * + *

This is intended for future evolution or custom driver extensions. The contents of this map + * are unspecified and may change at any point in time, always check for the existence of a key + * before using it. + * + *

Note that the returned map is immutable: if the properties change, this is reflected by + * publishing a new map instance, therefore you must call this method again to see the changes. + */ + @NonNull + Map getExtras(); + + @NonNull + NodeState getState(); + + /** + * The last time that this node transitioned to the UP state, in milliseconds since the epoch, or + * -1 if it's not up at the moment. + */ + long getUpSinceMillis(); + + /** + * The total number of active connections currently open by this driver instance to the node. This + * can be either pooled connections, or the control connection. + */ + int getOpenConnections(); + + /** + * Whether the driver is currently trying to reconnect to this node. That is, whether the active + * connection count is below the value mandated by the configuration. This does not mean that the + * node is down, there could be some active connections but not enough. + */ + boolean isReconnecting(); + + /** + * The distance assigned to this node by the {@link LoadBalancingPolicy}, that controls certain + * aspects of connection management. + * + *

This is exposed here for information only. Distance events are handled internally by the + * driver. + */ + @NonNull + NodeDistance getDistance(); + + /** + * The host ID that is assigned to this node by Cassandra. This value can be used to uniquely + * identify a node even when the underling IP address changes. + * + *

This should be non-null in a healthy deployment, but the driver will still function, and + * report {@code null} here, if the server metadata was corrupted. + */ + @Nullable + UUID getHostId(); + + /** + * The current version that is associated with the node's schema. + * + *

This should be non-null in a healthy deployment, but the driver will still function, and + * report {@code null} here, if the server metadata was corrupted. + */ + @Nullable + UUID getSchemaVersion(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/NodeState.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/NodeState.java new file mode 100644 index 00000000000..c35ea0ccacb --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/NodeState.java @@ -0,0 +1,66 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata; + +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.internal.core.metadata.TopologyEvent; +import java.net.InetSocketAddress; + +/** The state of a node, as viewed from the driver. */ +public enum NodeState { + /** + * The driver has never tried to connect to the node, nor received any topology events about it. + * + *

This happens when nodes are first added to the cluster, and will persist if your {@link + * LoadBalancingPolicy} decides to ignore them. Since the driver does not connect to them, the + * only way it can assess their states is from topology events. + */ + UNKNOWN, + /** + * A node is considered up in either of the following situations: + * + *

    + *
  • the driver has at least one active connection to the node. + *
  • the driver is not actively trying to connect to the node (because it's ignored by the + * {@link LoadBalancingPolicy}), but it has received a topology event indicating that the + * node is up. + *
+ */ + UP, + /** + * A node is considered down in either of the following situations: + * + *
    + *
  • the driver has lost all connections to the node (and is currently trying to reconnect). + *
  • the driver is not actively trying to connect to the node (because it's ignored by the + * {@link LoadBalancingPolicy}), but it has received a topology event indicating that the + * node is down. + *
+ */ + DOWN, + /** + * The node was forced down externally, the driver will never try to reconnect to it, whatever the + * {@link LoadBalancingPolicy} says. + * + *

This is used for edge error cases, for example when the driver detects that it's trying to + * connect to a node that does not belong to the Cassandra cluster (e.g. a wrong address was + * provided in the contact points). It can also be {@link + * TopologyEvent#forceDown(InetSocketAddress) triggered explicitly} by components (for example a + * custom load balancing policy) that want to limit the number of nodes that the driver connects + * to. + */ + FORCED_DOWN, +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/NodeStateListener.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/NodeStateListener.java new file mode 100644 index 00000000000..f4e5677ce9e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/NodeStateListener.java @@ -0,0 +1,66 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata; + +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A listener that gets notified when nodes states change. + * + *

An implementation of this interface can be registered in the configuration, or with {@link + * SessionBuilder#withNodeStateListener(NodeStateListener)}. + * + *

Note that the methods defined by this interface will be executed by internal driver threads, + * and are therefore expected to have short execution times. If you need to perform long + * computations or blocking calls in response to schema change events, it is strongly recommended to + * schedule them asynchronously on a separate thread provided by your application code. + * + *

If you implement this interface but don't need to implement all the methods, extend {@link + * NodeStateListenerBase}. + */ +public interface NodeStateListener extends AutoCloseable { + + /** + * Invoked when a node is first added to the cluster. + * + *

The node is not up yet at this point. {@link #onUp(Node)} will be notified later if the + * driver successfully connects to the node (provided that a session is opened and the node is not + * {@link NodeDistance#IGNORED ignored}), or receives a topology event for it. + * + *

This method is not invoked for the contact points provided at initialization. It is + * however for new nodes discovered during the full node list refresh after the first connection. + */ + void onAdd(@NonNull Node node); + + /** Invoked when a node's state switches to {@link NodeState#UP}. */ + void onUp(@NonNull Node node); + + /** + * Invoked when a node's state switches to {@link NodeState#DOWN} or {@link + * NodeState#FORCED_DOWN}. + */ + void onDown(@NonNull Node node); + + /** + * Invoked when a node leaves the cluster. + * + *

This can be triggered by a topology event, or during a full node list refresh if the node is + * absent from the new list. + */ + void onRemove(@NonNull Node node); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/NodeStateListenerBase.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/NodeStateListenerBase.java new file mode 100644 index 00000000000..6420ee0b53f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/NodeStateListenerBase.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Convenience class for listener implementations that that don't need to override all methods (all + * methods in this class are empty). + */ +public class NodeStateListenerBase implements NodeStateListener { + + @Override + public void onAdd(@NonNull Node node) { + // nothing to do + } + + @Override + public void onUp(@NonNull Node node) { + // nothing to do + } + + @Override + public void onDown(@NonNull Node node) { + // nothing to do + } + + @Override + public void onRemove(@NonNull Node node) { + // nothing to do + } + + @Override + public void close() throws Exception { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/TokenMap.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/TokenMap.java new file mode 100644 index 00000000000..319e3e82d8f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/TokenMap.java @@ -0,0 +1,151 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.util.Set; + +/** + * Utility component to work with the tokens of a given driver instance. + * + *

Note that the methods that take a keyspace argument are based on schema metadata, which can be + * disabled or restricted to a subset of keyspaces; therefore these methods might return empty + * results for some or all of the keyspaces. + * + * @see DefaultDriverOption#METADATA_SCHEMA_ENABLED + * @see Session#setSchemaMetadataEnabled(Boolean) + * @see DefaultDriverOption#METADATA_SCHEMA_REFRESHED_KEYSPACES + */ +public interface TokenMap { + + /** Builds a token from its string representation. */ + @NonNull + Token parse(@NonNull String tokenString); + + /** Formats a token into a string representation appropriate for concatenation in a CQL query. */ + @NonNull + String format(@NonNull Token token); + + /** + * Builds a token from a partition key. + * + * @param partitionKey the partition key components, in their serialized form (which can be + * obtained with {@link TypeCodec#encode(Object, ProtocolVersion)}. Neither the individual + * components, nor the vararg array itself, can be {@code null}. + */ + @NonNull + Token newToken(@NonNull ByteBuffer... partitionKey); + + @NonNull + TokenRange newTokenRange(@NonNull Token start, @NonNull Token end); + + /** The token ranges that define data distribution on the ring. */ + @NonNull + Set getTokenRanges(); + + /** The token ranges for which a given node is the primary replica. */ + @NonNull + Set getTokenRanges(Node node); + + /** + * The tokens owned by the given node. + * + *

This is functionally equivalent to {@code getTokenRanges(node).map(r -> r.getEnd())}. Note + * that the set is rebuilt every time you call this method. + */ + @NonNull + default Set getTokens(@NonNull Node node) { + ImmutableSet.Builder result = ImmutableSet.builder(); + for (TokenRange range : getTokenRanges(node)) { + result.add(range.getEnd()); + } + return result.build(); + } + + /** The token ranges that are replicated on the given node, for the given keyspace. */ + @NonNull + Set getTokenRanges(@NonNull CqlIdentifier keyspace, @NonNull Node replica); + + /** + * Shortcut for {@link #getTokenRanges(CqlIdentifier, Node) + * getTokenRanges(CqlIdentifier.fromCql(keyspaceName), replica)}. + */ + @NonNull + default Set getTokenRanges(@NonNull String keyspaceName, @NonNull Node replica) { + return getTokenRanges(CqlIdentifier.fromCql(keyspaceName), replica); + } + + /** The replicas for a given partition key in the given keyspace. */ + @NonNull + Set getReplicas(@NonNull CqlIdentifier keyspace, @NonNull ByteBuffer partitionKey); + + /** + * Shortcut for {@link #getReplicas(CqlIdentifier, ByteBuffer) + * getReplicas(CqlIdentifier.fromCql(keyspaceName), partitionKey)}. + */ + @NonNull + default Set getReplicas(@NonNull String keyspaceName, @NonNull ByteBuffer partitionKey) { + return getReplicas(CqlIdentifier.fromCql(keyspaceName), partitionKey); + } + + /** The replicas for a given token in the given keyspace. */ + @NonNull + Set getReplicas(@NonNull CqlIdentifier keyspace, @NonNull Token token); + + /** + * Shortcut for {@link #getReplicas(CqlIdentifier, Token) + * getReplicas(CqlIdentifier.fromCql(keyspaceName), token)}. + */ + @NonNull + default Set getReplicas(@NonNull String keyspaceName, @NonNull Token token) { + return getReplicas(CqlIdentifier.fromCql(keyspaceName), token); + } + + /** + * The replicas for a given range in the given keyspace. + * + *

It is assumed that the input range does not overlap across multiple node ranges. If the + * range extends over multiple nodes, it only returns the nodes that are replicas for the last + * token of the range. In other words, this method is a shortcut for {@code getReplicas(keyspace, + * range.getEnd())}. + */ + @NonNull + default Set getReplicas(@NonNull CqlIdentifier keyspace, @NonNull TokenRange range) { + return getReplicas(keyspace, range.getEnd()); + } + + /** + * Shortcut for {@link #getReplicas(CqlIdentifier, TokenRange) + * getReplicas(CqlIdentifier.fromCql(keyspaceName), range)}. + */ + @NonNull + default Set getReplicas(@NonNull String keyspaceName, @NonNull TokenRange range) { + return getReplicas(CqlIdentifier.fromCql(keyspaceName), range); + } + + /** The name of the partitioner class in use, as reported by the Cassandra nodes. */ + @NonNull + String getPartitionerName(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/AggregateMetadata.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/AggregateMetadata.java new file mode 100644 index 00000000000..acc06540225 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/AggregateMetadata.java @@ -0,0 +1,144 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.internal.core.metadata.schema.ScriptBuilder; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Optional; + +/** A CQL aggregate in the schema metadata. */ +public interface AggregateMetadata extends Describable { + + @NonNull + CqlIdentifier getKeyspace(); + + @NonNull + FunctionSignature getSignature(); + + /** + * The signature of the final function of this aggregate, or empty if there is none. + * + *

This is the function specified with {@code FINALFUNC} in the {@code CREATE AGGREGATE...} + * statement. It transforms the final value after the aggregation is complete. + */ + @NonNull + Optional getFinalFuncSignature(); + + /** + * The initial state value of this aggregate, or {@code null} if there is none. + * + *

This is the value specified with {@code INITCOND} in the {@code CREATE AGGREGATE...} + * statement. It's passed to the initial invocation of the state function (if that function does + * not accept null arguments). + * + *

The actual type of the returned object depends on the aggregate's {@link #getStateType() + * state type} and on the {@link TypeCodec codec} used to {@link TypeCodec#parse(String) parse} + * the {@code INITCOND} literal. + * + *

If, for some reason, the {@code INITCOND} literal cannot be parsed, a warning will be logged + * and the returned object will be the original {@code INITCOND} literal in its textual, + * non-parsed form. + * + * @return the initial state, or empty if there is none. + */ + @NonNull + Optional getInitCond(); + + /** + * The return type of this aggregate. + * + *

This is the final type of the value computed by this aggregate; in other words, the return + * type of the final function if it is defined, or the state type otherwise. + */ + @NonNull + DataType getReturnType(); + + /** + * The signature of the state function of this aggregate. + * + *

This is the function specified with {@code SFUNC} in the {@code CREATE AGGREGATE...} + * statement. It aggregates the current state with each row to produce a new state. + */ + @NonNull + FunctionSignature getStateFuncSignature(); + + /** + * The state type of this aggregate. + * + *

This is the type specified with {@code STYPE} in the {@code CREATE AGGREGATE...} statement. + * It defines the type of the value that is accumulated as the aggregate iterates through the + * rows. + */ + @NonNull + DataType getStateType(); + + @NonNull + @Override + default String describeWithChildren(boolean pretty) { + // An aggregate has no children + return describe(pretty); + } + + @NonNull + @Override + default String describe(boolean pretty) { + ScriptBuilder builder = new ScriptBuilder(pretty); + builder + .append("CREATE AGGREGATE ") + .append(getKeyspace()) + .append(".") + .append(getSignature().getName()) + .append("("); + boolean first = true; + for (int i = 0; i < getSignature().getParameterTypes().size(); i++) { + if (first) { + first = false; + } else { + builder.append(","); + } + DataType type = getSignature().getParameterTypes().get(i); + builder.append(type.asCql(false, pretty)); + } + builder + .increaseIndent() + .append(")") + .newLine() + .append("SFUNC ") + .append(getStateFuncSignature().getName()) + .newLine() + .append("STYPE ") + .append(getStateType().asCql(false, pretty)); + + if (getFinalFuncSignature().isPresent()) { + builder.newLine().append("FINALFUNC ").append(getFinalFuncSignature().get().getName()); + } + if (getInitCond().isPresent()) { + Optional formatInitCond = formatInitCond(); + assert formatInitCond.isPresent(); + builder.newLine().append("INITCOND ").append(formatInitCond.get()); + } + return builder.append(";").build(); + } + + /** + * Formats the {@linkplain #getInitCond() initial state value} for inclusion in a CQL statement. + */ + @NonNull + Optional formatInitCond(); +} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ClusteringOrder.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/ClusteringOrder.java similarity index 75% rename from driver-core/src/main/java/com/datastax/driver/core/ClusteringOrder.java rename to core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/ClusteringOrder.java index 0cf79e64300..ce50cc6ef40 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/ClusteringOrder.java +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/ClusteringOrder.java @@ -13,15 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.datastax.driver.core; +package com.datastax.oss.driver.api.core.metadata.schema; -/** - * Clustering orders. - * - *

This is used by metadata classes to indicate the clustering order of a clustering column in a - * table or materialized view. - */ +/** The order of a clustering column in a table or materialized view. */ public enum ClusteringOrder { ASC, - DESC; + DESC } diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/ColumnMetadata.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/ColumnMetadata.java new file mode 100644 index 00000000000..236cfd9b385 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/ColumnMetadata.java @@ -0,0 +1,42 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** A column in the schema metadata. */ +public interface ColumnMetadata { + + @NonNull + CqlIdentifier getKeyspace(); + + /** + * The identifier of the {@link TableMetadata} or a {@link ViewMetadata} that this column belongs + * to. + */ + @NonNull + CqlIdentifier getParent(); + + @NonNull + CqlIdentifier getName(); + + @NonNull + DataType getType(); + + boolean isStatic(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/Describable.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/Describable.java new file mode 100644 index 00000000000..3512c9a5560 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/Describable.java @@ -0,0 +1,44 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** A schema element that can be described in terms of CQL {@code CREATE} statements. */ +public interface Describable { + + /** + * Returns a single CQL statement that creates the element. + * + * @param pretty if {@code true}, make the output more human-readable (line breaks, indents, and + * {@link CqlIdentifier#asCql(boolean) pretty identifiers}). If {@code false}, return the + * statement on a single line with minimal formatting. + */ + @NonNull + String describe(boolean pretty); + + /** + * Returns a CQL script that creates the element and all of its children. For example: a schema + * with its tables, materialized views, types, etc. A table with its indices. + * + * @param pretty if {@code true}, make the output more human-readable (line breaks, indents, and + * {@link CqlIdentifier#asCql(boolean) pretty identifiers}). If {@code false}, return each + * statement on a single line with minimal formatting. + */ + @NonNull + String describeWithChildren(boolean pretty); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/FunctionMetadata.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/FunctionMetadata.java new file mode 100644 index 00000000000..ff273c34f81 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/FunctionMetadata.java @@ -0,0 +1,96 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.internal.core.metadata.schema.ScriptBuilder; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +/** A CQL function in the schema metadata. */ +public interface FunctionMetadata extends Describable { + + @NonNull + CqlIdentifier getKeyspace(); + + @NonNull + FunctionSignature getSignature(); + + /** + * The names of the parameters. This is in the same order as {@code + * getSignature().getParameterTypes()} + */ + @NonNull + List getParameterNames(); + + @NonNull + String getBody(); + + boolean isCalledOnNullInput(); + + @NonNull + String getLanguage(); + + @NonNull + DataType getReturnType(); + + @NonNull + @Override + default String describe(boolean pretty) { + ScriptBuilder builder = new ScriptBuilder(pretty); + builder + .append("CREATE FUNCTION ") + .append(getKeyspace()) + .append(".") + .append(getSignature().getName()) + .append("("); + boolean first = true; + for (int i = 0; i < getSignature().getParameterTypes().size(); i++) { + if (first) { + first = false; + } else { + builder.append(","); + } + DataType type = getSignature().getParameterTypes().get(i); + CqlIdentifier name = getParameterNames().get(i); + builder.append(name).append(" ").append(type.asCql(false, pretty)); + } + return builder + .append(")") + .increaseIndent() + .newLine() + .append(isCalledOnNullInput() ? "CALLED ON NULL INPUT" : "RETURNS NULL ON NULL INPUT") + .newLine() + .append("RETURNS ") + .append(getReturnType().asCql(false, true)) + .newLine() + .append("LANGUAGE ") + .append(getLanguage()) + .newLine() + .append("AS '") + .append(getBody()) + .append("';") + .build(); + } + + @NonNull + @Override + default String describeWithChildren(boolean pretty) { + // A function has no children + return describe(pretty); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/FunctionSignature.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/FunctionSignature.java new file mode 100644 index 00000000000..14bb7947a60 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/FunctionSignature.java @@ -0,0 +1,113 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +/** + * The signature that uniquely identifies a CQL function or aggregate in a keyspace. + * + *

It's composed of a name and a list of parameter types. Overloads (such as {@code sum(int)} and + * {@code sum(int, int)} are not equal. + */ +@Immutable +public class FunctionSignature { + @NonNull private final CqlIdentifier name; + @NonNull private final List parameterTypes; + + public FunctionSignature( + @NonNull CqlIdentifier name, @NonNull Iterable parameterTypes) { + this.name = name; + this.parameterTypes = ImmutableList.copyOf(parameterTypes); + } + + /** + * @param parameterTypes neither the individual types, nor the vararg array itself, can be null. + */ + public FunctionSignature(@NonNull CqlIdentifier name, @NonNull DataType... parameterTypes) { + this( + name, + parameterTypes.length == 0 + ? ImmutableList.of() + : ImmutableList.builder().add(parameterTypes).build()); + } + + /** + * Shortcut for {@link #FunctionSignature(CqlIdentifier, Iterable) new + * FunctionSignature(CqlIdentifier.fromCql(name), parameterTypes)}. + */ + public FunctionSignature(@NonNull String name, @NonNull Iterable parameterTypes) { + this(CqlIdentifier.fromCql(name), parameterTypes); + } + + /** + * Shortcut for {@link #FunctionSignature(CqlIdentifier, DataType...)} new + * FunctionSignature(CqlIdentifier.fromCql(name), parameterTypes)}. + * + * @param parameterTypes neither the individual types, nor the vararg array itself, can be null. + */ + public FunctionSignature(@NonNull String name, @NonNull DataType... parameterTypes) { + this(CqlIdentifier.fromCql(name), parameterTypes); + } + + @NonNull + public CqlIdentifier getName() { + return name; + } + + @NonNull + public List getParameterTypes() { + return parameterTypes; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof FunctionSignature) { + FunctionSignature that = (FunctionSignature) other; + return this.name.equals(that.name) && this.parameterTypes.equals(that.parameterTypes); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(name, parameterTypes); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(name.asInternal()).append('('); + boolean first = true; + for (DataType type : parameterTypes) { + if (first) { + first = false; + } else { + builder.append(", "); + } + builder.append(type.asCql(true, true)); + } + return builder.append(')').toString(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/IndexKind.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/IndexKind.java new file mode 100644 index 00000000000..941b67587ba --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/IndexKind.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +/** A kind of index in the schema. */ +public enum IndexKind { + KEYS, + CUSTOM, + COMPOSITES +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/IndexMetadata.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/IndexMetadata.java new file mode 100644 index 00000000000..773eba5cb8b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/IndexMetadata.java @@ -0,0 +1,113 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.core.metadata.schema.ScriptBuilder; +import com.datastax.oss.driver.shaded.guava.common.collect.Maps; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import java.util.Optional; + +/** A secondary index in the schema metadata. */ +public interface IndexMetadata extends Describable { + + @NonNull + CqlIdentifier getKeyspace(); + + @NonNull + CqlIdentifier getTable(); + + @NonNull + CqlIdentifier getName(); + + @NonNull + IndexKind getKind(); + + @NonNull + String getTarget(); + + /** If this index is custom, the name of the server-side implementation. Otherwise, empty. */ + @NonNull + default Optional getClassName() { + return Optional.ofNullable(getOptions().get("class_name")); + } + + /** + * The options of the index. + * + *

This directly reflects the corresponding column of the system table ({@code + * system.schema_columns.index_options} in Cassandra <= 2.2, or {@code + * system_schema.indexes.options} in later versions). + * + *

Note that some of these options might also be exposed as standalone fields in this + * interface, namely {@link #getClassName()} and {{@link #getTarget()}}. + */ + @NonNull + Map getOptions(); + + @NonNull + @Override + default String describe(boolean pretty) { + ScriptBuilder builder = new ScriptBuilder(pretty); + if (getClassName().isPresent()) { + builder + .append("CREATE CUSTOM INDEX ") + .append(getName()) + .append(" ON ") + .append(getKeyspace()) + .append(".") + .append(getTable()) + .append(String.format(" (%s)", getTarget())) + .newLine() + .append(String.format("USING '%s'", getClassName())); + + // Some options already appear in the CREATE statement, ignore them + Map describedOptions = + Maps.filterKeys(getOptions(), k -> !"target".equals(k) && !"class_name".equals(k)); + if (!describedOptions.isEmpty()) { + builder.newLine().append("WITH OPTIONS = {").newLine().increaseIndent(); + boolean first = true; + for (Map.Entry option : describedOptions.entrySet()) { + if (first) { + first = false; + } else { + builder.append(",").newLine(); + } + builder.append(String.format("'%s' : '%s'", option.getKey(), option.getValue())); + } + builder.decreaseIndent().append("}"); + } + } else { + builder + .append("CREATE INDEX ") + .append(getName()) + .append(" ON ") + .append(getKeyspace()) + .append(".") + .append(getTable()) + .append(String.format(" (%s);", getTarget())); + } + return builder.build(); + } + + @NonNull + @Override + default String describeWithChildren(boolean pretty) { + // An index has no children + return describe(pretty); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/KeyspaceMetadata.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/KeyspaceMetadata.java new file mode 100644 index 00000000000..abda435ba02 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/KeyspaceMetadata.java @@ -0,0 +1,248 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.metadata.schema.ScriptBuilder; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterables; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import java.util.Optional; + +/** A keyspace in the schema metadata. */ +public interface KeyspaceMetadata extends Describable { + + @NonNull + CqlIdentifier getName(); + + /** Whether durable writes are set on this keyspace. */ + boolean isDurableWrites(); + + /** Whether this keyspace is virtual */ + boolean isVirtual(); + + /** The replication options defined for this keyspace. */ + @NonNull + Map getReplication(); + + @NonNull + Map getTables(); + + @NonNull + default Optional getTable(@NonNull CqlIdentifier tableId) { + return Optional.ofNullable(getTables().get(tableId)); + } + + /** Shortcut for {@link #getTable(CqlIdentifier) getTable(CqlIdentifier.fromCql(tableName))}. */ + @NonNull + default Optional getTable(@NonNull String tableName) { + return getTable(CqlIdentifier.fromCql(tableName)); + } + + @NonNull + Map getViews(); + + /** Gets the views based on a given table. */ + @NonNull + default Map getViewsOnTable(@NonNull CqlIdentifier tableId) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (ViewMetadata view : getViews().values()) { + if (view.getBaseTable().equals(tableId)) { + builder.put(view.getName(), view); + } + } + return builder.build(); + } + + @NonNull + default Optional getView(@NonNull CqlIdentifier viewId) { + return Optional.ofNullable(getViews().get(viewId)); + } + + /** Shortcut for {@link #getView(CqlIdentifier) getView(CqlIdentifier.fromCql(viewName))}. */ + @NonNull + default Optional getView(@NonNull String viewName) { + return getView(CqlIdentifier.fromCql(viewName)); + } + + @NonNull + Map getUserDefinedTypes(); + + @NonNull + default Optional getUserDefinedType(@NonNull CqlIdentifier typeId) { + return Optional.ofNullable(getUserDefinedTypes().get(typeId)); + } + + /** + * Shortcut for {@link #getUserDefinedType(CqlIdentifier) + * getUserDefinedType(CqlIdentifier.fromCql(typeName))}. + */ + @NonNull + default Optional getUserDefinedType(@NonNull String typeName) { + return getUserDefinedType(CqlIdentifier.fromCql(typeName)); + } + + @NonNull + Map getFunctions(); + + @NonNull + default Optional getFunction(@NonNull FunctionSignature functionSignature) { + return Optional.ofNullable(getFunctions().get(functionSignature)); + } + + @NonNull + default Optional getFunction( + @NonNull CqlIdentifier functionId, @NonNull Iterable parameterTypes) { + return Optional.ofNullable( + getFunctions().get(new FunctionSignature(functionId, parameterTypes))); + } + + /** + * Shortcut for {@link #getFunction(CqlIdentifier, Iterable) + * getFunction(CqlIdentifier.fromCql(functionName), parameterTypes)}. + */ + @NonNull + default Optional getFunction( + @NonNull String functionName, @NonNull Iterable parameterTypes) { + return getFunction(CqlIdentifier.fromCql(functionName), parameterTypes); + } + + /** + * @param parameterTypes neither the individual types, nor the vararg array itself, can be null. + */ + @NonNull + default Optional getFunction( + @NonNull CqlIdentifier functionId, @NonNull DataType... parameterTypes) { + return Optional.ofNullable( + getFunctions().get(new FunctionSignature(functionId, parameterTypes))); + } + + /** + * Shortcut for {@link #getFunction(CqlIdentifier, DataType...) + * getFunction(CqlIdentifier.fromCql(functionName), parameterTypes)}. + * + * @param parameterTypes neither the individual types, nor the vararg array itself, can be null. + */ + @NonNull + default Optional getFunction( + @NonNull String functionName, @NonNull DataType... parameterTypes) { + return getFunction(CqlIdentifier.fromCql(functionName), parameterTypes); + } + + @NonNull + Map getAggregates(); + + @NonNull + default Optional getAggregate(@NonNull FunctionSignature aggregateSignature) { + return Optional.ofNullable(getAggregates().get(aggregateSignature)); + } + + @NonNull + default Optional getAggregate( + @NonNull CqlIdentifier aggregateId, @NonNull Iterable parameterTypes) { + return Optional.ofNullable( + getAggregates().get(new FunctionSignature(aggregateId, parameterTypes))); + } + + /** + * Shortcut for {@link #getAggregate(CqlIdentifier, Iterable) + * getAggregate(CqlIdentifier.fromCql(aggregateName), parameterTypes)}. + */ + @NonNull + default Optional getAggregate( + @NonNull String aggregateName, @NonNull Iterable parameterTypes) { + return getAggregate(CqlIdentifier.fromCql(aggregateName), parameterTypes); + } + + /** + * @param parameterTypes neither the individual types, nor the vararg array itself, can be null. + */ + @NonNull + default Optional getAggregate( + @NonNull CqlIdentifier aggregateId, @NonNull DataType... parameterTypes) { + return Optional.ofNullable( + getAggregates().get(new FunctionSignature(aggregateId, parameterTypes))); + } + + /** + * Shortcut for {@link #getAggregate(CqlIdentifier, DataType...)} + * getAggregate(CqlIdentifier.fromCql(aggregateName), parameterTypes)}. + * + * @param parameterTypes neither the individual types, nor the vararg array itself, can be null. + */ + @NonNull + default Optional getAggregate( + @NonNull String aggregateName, @NonNull DataType... parameterTypes) { + return getAggregate(CqlIdentifier.fromCql(aggregateName), parameterTypes); + } + + @NonNull + @Override + default String describe(boolean pretty) { + ScriptBuilder builder = new ScriptBuilder(pretty); + if (isVirtual()) { + builder.append("/* VIRTUAL "); + } else { + builder.append("CREATE "); + } + builder + .append("KEYSPACE ") + .append(getName()) + .append(" WITH replication = { 'class' : '") + .append(getReplication().get("class")) + .append("'"); + for (Map.Entry entry : getReplication().entrySet()) { + if (!entry.getKey().equals("class")) { + builder + .append(", '") + .append(entry.getKey()) + .append("': '") + .append(entry.getValue()) + .append("'"); + } + } + builder + .append(" } AND durable_writes = ") + .append(Boolean.toString(isDurableWrites())) + .append(";"); + if (isVirtual()) { + builder.append(" */"); + } + return builder.build(); + } + + @NonNull + @Override + default String describeWithChildren(boolean pretty) { + String createKeyspace = describe(pretty); + ScriptBuilder builder = new ScriptBuilder(pretty).append(createKeyspace); + + for (Describable element : + Iterables.concat( + getUserDefinedTypes().values(), + getTables().values(), + getViews().values(), + getFunctions().values(), + getAggregates().values())) { + builder.forceNewLine(2).append(element.describeWithChildren(pretty)); + } + + return builder.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/RelationMetadata.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/RelationMetadata.java new file mode 100644 index 00000000000..2cf5a803aa0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/RelationMetadata.java @@ -0,0 +1,87 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +/** A table or materialized view in the schema metadata. */ +public interface RelationMetadata extends Describable { + + @NonNull + CqlIdentifier getKeyspace(); + + @NonNull + CqlIdentifier getName(); + + /** The unique id generated by the server for this element. */ + Optional getId(); + + /** + * Convenience method to get all the primary key columns (partition key + clustering columns) in a + * single call. + * + *

Note that this creates a new list instance on each call. + * + * @see #getPartitionKey() + * @see #getClusteringColumns() + */ + @NonNull + default List getPrimaryKey() { + return ImmutableList.builder() + .addAll(getPartitionKey()) + .addAll(getClusteringColumns().keySet()) + .build(); + } + + @NonNull + List getPartitionKey(); + + @NonNull + Map getClusteringColumns(); + + @NonNull + Map getColumns(); + + @NonNull + default Optional getColumn(@NonNull CqlIdentifier columnId) { + return Optional.ofNullable(getColumns().get(columnId)); + } + + /** + * Shortcut for {@link #getColumn(CqlIdentifier) getColumn(CqlIdentifier.fromCql(columnName))}. + */ + @NonNull + default Optional getColumn(@NonNull String columnName) { + return getColumn(CqlIdentifier.fromCql(columnName)); + } + + /** + * The options of this table or materialized view. + * + *

This corresponds to the {@code WITH} clauses in the {@code CREATE} statement that would + * recreate this element. The exact set of keys and the types of the values depend on the server + * version that this metadata was extracted from. For example, in Cassandra 2.2 and below, {@code + * WITH caching} takes a string argument, whereas starting with Cassandra 3.0 it is a map. + */ + @NonNull + Map getOptions(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/SchemaChangeListener.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/SchemaChangeListener.java new file mode 100644 index 00000000000..52a4208c813 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/SchemaChangeListener.java @@ -0,0 +1,74 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Tracks schema changes. + * + *

An implementation of this interface can be registered in the configuration, or with {@link + * SessionBuilder#withSchemaChangeListener(SchemaChangeListener)}. + * + *

Note that the methods defined by this interface will be executed by internal driver threads, + * and are therefore expected to have short execution times. If you need to perform long + * computations or blocking calls in response to schema change events, it is strongly recommended to + * schedule them asynchronously on a separate thread provided by your application code. + * + *

If you implement this interface but don't need to implement all the methods, extend {@link + * SchemaChangeListenerBase}. + */ +public interface SchemaChangeListener extends AutoCloseable { + + void onKeyspaceCreated(@NonNull KeyspaceMetadata keyspace); + + void onKeyspaceDropped(@NonNull KeyspaceMetadata keyspace); + + void onKeyspaceUpdated(@NonNull KeyspaceMetadata current, @NonNull KeyspaceMetadata previous); + + void onTableCreated(@NonNull TableMetadata table); + + void onTableDropped(@NonNull TableMetadata table); + + void onTableUpdated(@NonNull TableMetadata current, @NonNull TableMetadata previous); + + void onUserDefinedTypeCreated(@NonNull UserDefinedType type); + + void onUserDefinedTypeDropped(@NonNull UserDefinedType type); + + void onUserDefinedTypeUpdated( + @NonNull UserDefinedType current, @NonNull UserDefinedType previous); + + void onFunctionCreated(@NonNull FunctionMetadata function); + + void onFunctionDropped(@NonNull FunctionMetadata function); + + void onFunctionUpdated(@NonNull FunctionMetadata current, @NonNull FunctionMetadata previous); + + void onAggregateCreated(@NonNull AggregateMetadata aggregate); + + void onAggregateDropped(@NonNull AggregateMetadata aggregate); + + void onAggregateUpdated(@NonNull AggregateMetadata current, @NonNull AggregateMetadata previous); + + void onViewCreated(@NonNull ViewMetadata view); + + void onViewDropped(@NonNull ViewMetadata view); + + void onViewUpdated(@NonNull ViewMetadata current, @NonNull ViewMetadata previous); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/SchemaChangeListenerBase.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/SchemaChangeListenerBase.java new file mode 100644 index 00000000000..0d4adef2878 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/SchemaChangeListenerBase.java @@ -0,0 +1,125 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Convenience class for listener implementations that that don't need to override all methods (all + * methods in this class are empty). + */ +public class SchemaChangeListenerBase implements SchemaChangeListener { + + @Override + public void onKeyspaceCreated(@NonNull KeyspaceMetadata keyspace) { + // nothing to do + } + + @Override + public void onKeyspaceDropped(@NonNull KeyspaceMetadata keyspace) { + // nothing to do + } + + @Override + public void onKeyspaceUpdated( + @NonNull KeyspaceMetadata current, @NonNull KeyspaceMetadata previous) { + // nothing to do + } + + @Override + public void onTableCreated(@NonNull TableMetadata table) { + // nothing to do + } + + @Override + public void onTableDropped(@NonNull TableMetadata table) { + // nothing to do + } + + @Override + public void onTableUpdated(@NonNull TableMetadata current, @NonNull TableMetadata previous) { + // nothing to do + } + + @Override + public void onUserDefinedTypeCreated(@NonNull UserDefinedType type) { + // nothing to do + } + + @Override + public void onUserDefinedTypeDropped(@NonNull UserDefinedType type) { + // nothing to do + } + + @Override + public void onUserDefinedTypeUpdated( + @NonNull UserDefinedType current, @NonNull UserDefinedType previous) { + // nothing to do + } + + @Override + public void onFunctionCreated(@NonNull FunctionMetadata function) { + // nothing to do + } + + @Override + public void onFunctionDropped(@NonNull FunctionMetadata function) { + // nothing to do + } + + @Override + public void onFunctionUpdated( + @NonNull FunctionMetadata current, @NonNull FunctionMetadata previous) { + // nothing to do + } + + @Override + public void onAggregateCreated(@NonNull AggregateMetadata aggregate) { + // nothing to do + } + + @Override + public void onAggregateDropped(@NonNull AggregateMetadata aggregate) { + // nothing to do + } + + @Override + public void onAggregateUpdated( + @NonNull AggregateMetadata current, @NonNull AggregateMetadata previous) { + // nothing to do + } + + @Override + public void onViewCreated(@NonNull ViewMetadata view) { + // nothing to do + } + + @Override + public void onViewDropped(@NonNull ViewMetadata view) { + // nothing to do + } + + @Override + public void onViewUpdated(@NonNull ViewMetadata current, @NonNull ViewMetadata previous) { + // nothing to do + } + + @Override + public void close() throws Exception { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/TableMetadata.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/TableMetadata.java new file mode 100644 index 00000000000..425d08945c0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/TableMetadata.java @@ -0,0 +1,129 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.core.metadata.schema.ScriptBuilder; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.RelationParser; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; + +/** A table in the schema metadata. */ +public interface TableMetadata extends RelationMetadata { + + boolean isCompactStorage(); + + /** Whether this table is virtual */ + boolean isVirtual(); + + @NonNull + Map getIndexes(); + + @NonNull + @Override + default String describe(boolean pretty) { + ScriptBuilder builder = new ScriptBuilder(pretty); + if (isVirtual()) { + builder.append("/* VIRTUAL "); + } else { + builder.append("CREATE "); + } + + builder + .append("TABLE ") + .append(getKeyspace()) + .append(".") + .append(getName()) + .append(" (") + .newLine() + .increaseIndent(); + + for (ColumnMetadata column : getColumns().values()) { + builder.append(column.getName()).append(" ").append(column.getType().asCql(true, pretty)); + if (column.isStatic()) { + builder.append(" static"); + } + builder.append(",").newLine(); + } + + // PK + builder.append("PRIMARY KEY ("); + if (getPartitionKey().size() == 1) { // PRIMARY KEY (k + builder.append(getPartitionKey().get(0).getName()); + } else { // PRIMARY KEY ((k1, k2) + builder.append("("); + boolean first = true; + for (ColumnMetadata pkColumn : getPartitionKey()) { + if (first) { + first = false; + } else { + builder.append(", "); + } + builder.append(pkColumn.getName()); + } + builder.append(")"); + } + // PRIMARY KEY (, cc1, cc2, cc3) + for (ColumnMetadata clusteringColumn : getClusteringColumns().keySet()) { + builder.append(", ").append(clusteringColumn.getName()); + } + builder.append(")"); + + builder.newLine().decreaseIndent().append(")"); + + builder.increaseIndent(); + if (isCompactStorage()) { + builder.andWith().append("COMPACT STORAGE"); + } + if (getClusteringColumns().containsValue(ClusteringOrder.DESC)) { + builder.andWith().append("CLUSTERING ORDER BY ("); + boolean first = true; + for (Map.Entry entry : getClusteringColumns().entrySet()) { + if (first) { + first = false; + } else { + builder.append(", "); + } + builder.append(entry.getKey().getName()).append(" ").append(entry.getValue().name()); + } + builder.append(")"); + } + Map options = getOptions(); + RelationParser.appendOptions(options, builder); + builder.append(";"); + if (isVirtual()) { + builder.append(" */"); + } + return builder.build(); + } + + /** + * {@inheritDoc} + * + *

This describes the table and all of its indices. Contrary to previous driver versions, views + * are not included. + */ + @NonNull + @Override + default String describeWithChildren(boolean pretty) { + String createTable = describe(pretty); + ScriptBuilder builder = new ScriptBuilder(pretty).append(createTable); + for (IndexMetadata indexMetadata : getIndexes().values()) { + builder.forceNewLine(2).append(indexMetadata.describeWithChildren(pretty)); + } + return builder.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/ViewMetadata.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/ViewMetadata.java new file mode 100644 index 00000000000..6d6b599e355 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/schema/ViewMetadata.java @@ -0,0 +1,107 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.core.metadata.schema.ScriptBuilder; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.RelationParser; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Optional; + +/** A materialized view in the schema metadata. */ +public interface ViewMetadata extends RelationMetadata { + + /** The table that this view is based on. */ + @NonNull + CqlIdentifier getBaseTable(); + + /** + * Whether this view does a {@code SELECT *} on its base table (this only affects the output of + * {@link #describe(boolean)}). + */ + boolean includesAllColumns(); + + @NonNull + Optional getWhereClause(); + + @NonNull + @Override + default String describe(boolean pretty) { + ScriptBuilder builder = + new ScriptBuilder(pretty) + .append("CREATE MATERIALIZED VIEW ") + .append(getKeyspace()) + .append(".") + .append(getName()) + .append(" AS") + .newLine(); + + builder.append("SELECT"); + if (includesAllColumns()) { + builder.append(" * "); + } else { + builder.newLine().increaseIndent(); + boolean first = true; + for (ColumnMetadata column : getColumns().values()) { + if (first) { + first = false; + } else { + builder.append(",").newLine(); + } + builder.append(column.getName()); + } + builder.newLine().decreaseIndent(); + } + + builder.append("FROM ").append(getKeyspace()).append(".").append(getBaseTable()); + + Optional whereClause = getWhereClause(); + if (whereClause.isPresent() && !whereClause.get().isEmpty()) { + builder.newLine().append("WHERE ").append(whereClause.get()); + } + + builder.newLine().append("PRIMARY KEY ("); + if (getPartitionKey().size() == 1) { // PRIMARY KEY (k + builder.append(getPartitionKey().get(0).getName()); + } else { // PRIMARY KEY ((k1, k2) + builder.append("("); + boolean first = true; + for (ColumnMetadata pkColumn : getPartitionKey()) { + if (first) { + first = false; + } else { + builder.append(", "); + } + builder.append(pkColumn.getName()); + } + builder.append(")"); + } + // PRIMARY KEY (, cc1, cc2, cc3) + for (ColumnMetadata clusteringColumn : getClusteringColumns().keySet()) { + builder.append(", ").append(clusteringColumn.getName()); + } + builder.append(")").increaseIndent(); + + RelationParser.appendOptions(getOptions(), builder); + return builder.append(";").build(); + } + + @NonNull + @Override + default String describeWithChildren(boolean pretty) { + return describe(pretty); // A view has no children + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/token/Token.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/token/Token.java new file mode 100644 index 00000000000..af41553b7b4 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/token/Token.java @@ -0,0 +1,19 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.token; + +/** A token on the ring. */ +public interface Token extends Comparable {} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metadata/token/TokenRange.java b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/token/TokenRange.java new file mode 100644 index 00000000000..740c54caebc --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metadata/token/TokenRange.java @@ -0,0 +1,147 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metadata.token; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +/** + * A range of tokens on the Cassandra ring. + * + *

A range is start-exclusive and end-inclusive. It is empty when start and end are the same + * token, except if that is the minimum token, in which case the range covers the whole ring (this + * is consistent with the behavior of CQL range queries). + * + *

Note that CQL does not handle wrapping. To query all partitions in a range, see {@link + * #unwrap()}. + */ +public interface TokenRange extends Comparable { + + /** The start of the range (exclusive). */ + @NonNull + Token getStart(); + + /** The end of the range (inclusive). */ + @NonNull + Token getEnd(); + + /** + * Splits this range into a number of smaller ranges of equal "size" (referring to the number of + * tokens, not the actual amount of data). + * + *

Splitting an empty range is not permitted. But note that, in edge cases, splitting a range + * might produce one or more empty ranges. + * + * @throws IllegalArgumentException if the range is empty or if {@code numberOfSplits < 1}. + */ + @NonNull + List splitEvenly(int numberOfSplits); + + /** + * Whether this range is empty. + * + *

A range is empty when {@link #getStart()} and {@link #getEnd()} are the same token, except + * if that is the minimum token, in which case the range covers the whole ring (this is consistent + * with the behavior of CQL range queries). + */ + boolean isEmpty(); + + /** Whether this range wraps around the end of the ring. */ + boolean isWrappedAround(); + + /** Whether this range represents the full ring. */ + boolean isFullRing(); + + /** + * Splits this range into a list of two non-wrapping ranges. This will return the range itself if + * it is non-wrapping, or two ranges otherwise. + * + *

For example: + * + *

    + *
  • {@code ]1,10]} unwraps to itself; + *
  • {@code ]10,1]} unwraps to {@code ]10,min_token]} and {@code ]min_token,1]}. + *
+ * + *

This is useful for CQL range queries, which do not handle wrapping: + * + *

{@code
+   * List rows = new ArrayList();
+   * for (TokenRange subRange : range.unwrap()) {
+   *     ResultSet rs = session.execute(
+   *         "SELECT * FROM mytable WHERE token(pk) > ? and token(pk) <= ?",
+   *         subRange.getStart(), subRange.getEnd());
+   *     rows.addAll(rs.all());
+   * }
+   * }
+ */ + @NonNull + List unwrap(); + + /** + * Whether this range intersects another one. + * + *

For example: + * + *

    + *
  • {@code ]3,5]} intersects {@code ]1,4]}, {@code ]4,5]}... + *
  • {@code ]3,5]} does not intersect {@code ]1,2]}, {@code ]2,3]}, {@code ]5,7]}... + *
+ */ + boolean intersects(@NonNull TokenRange that); + + /** + * Computes the intersection of this range with another one, producing one or more ranges. + * + *

If either of these ranges overlap the the ring, they are unwrapped and the unwrapped ranges + * are compared to one another. + * + *

This call will fail if the two ranges do not intersect, you must check by calling {@link + * #intersects(TokenRange)} first. + * + * @param that the other range. + * @return the range(s) resulting from the intersection. + * @throws IllegalArgumentException if the ranges do not intersect. + */ + @NonNull + List intersectWith(@NonNull TokenRange that); + + /** + * Checks whether this range contains a given token, i.e. {@code range.start < token <= + * range.end}. + */ + boolean contains(@NonNull Token token); + + /** + * Merges this range with another one. + * + *

The two ranges should either intersect or be adjacent; in other words, the merged range + * should not include tokens that are in neither of the original ranges. + * + *

For example: + * + *

    + *
  • merging {@code ]3,5]} with {@code ]4,7]} produces {@code ]3,7]}; + *
  • merging {@code ]3,5]} with {@code ]4,5]} produces {@code ]3,5]}; + *
  • merging {@code ]3,5]} with {@code ]5,8]} produces {@code ]3,8]}; + *
  • merging {@code ]3,5]} with {@code ]6,8]} fails. + *
+ * + * @throws IllegalArgumentException if the ranges neither intersect nor are adjacent. + */ + @NonNull + TokenRange mergeWith(@NonNull TokenRange that); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metrics/DefaultNodeMetric.java b/core/src/main/java/com/datastax/oss/driver/api/core/metrics/DefaultNodeMetric.java new file mode 100644 index 00000000000..c42fa6ca0ba --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metrics/DefaultNodeMetric.java @@ -0,0 +1,84 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metrics; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; + +/** See {@code reference.conf} for a description of each metric. */ +public enum DefaultNodeMetric implements NodeMetric { + OPEN_CONNECTIONS("pool.open-connections"), + AVAILABLE_STREAMS("pool.available-streams"), + IN_FLIGHT("pool.in-flight"), + ORPHANED_STREAMS("pool.orphaned-streams"), + BYTES_SENT("bytes-sent"), + BYTES_RECEIVED("bytes-received"), + CQL_MESSAGES("cql-messages"), + UNSENT_REQUESTS("errors.request.unsent"), + ABORTED_REQUESTS("errors.request.aborted"), + WRITE_TIMEOUTS("errors.request.write-timeouts"), + READ_TIMEOUTS("errors.request.read-timeouts"), + UNAVAILABLES("errors.request.unavailables"), + OTHER_ERRORS("errors.request.others"), + RETRIES("retries.total"), + RETRIES_ON_ABORTED("retries.aborted"), + RETRIES_ON_READ_TIMEOUT("retries.read-timeout"), + RETRIES_ON_WRITE_TIMEOUT("retries.write-timeout"), + RETRIES_ON_UNAVAILABLE("retries.unavailable"), + RETRIES_ON_OTHER_ERROR("retries.other"), + IGNORES("ignores.total"), + IGNORES_ON_ABORTED("ignores.aborted"), + IGNORES_ON_READ_TIMEOUT("ignores.read-timeout"), + IGNORES_ON_WRITE_TIMEOUT("ignores.write-timeout"), + IGNORES_ON_UNAVAILABLE("ignores.unavailable"), + IGNORES_ON_OTHER_ERROR("ignores.other"), + SPECULATIVE_EXECUTIONS("speculative-executions"), + CONNECTION_INIT_ERRORS("errors.connection.init"), + AUTHENTICATION_ERRORS("errors.connection.auth"), + ; + + private static final Map BY_PATH = sortByPath(); + + private final String path; + + DefaultNodeMetric(String path) { + this.path = path; + } + + @Override + @NonNull + public String getPath() { + return path; + } + + @NonNull + public static DefaultNodeMetric fromPath(@NonNull String path) { + DefaultNodeMetric metric = BY_PATH.get(path); + if (metric == null) { + throw new IllegalArgumentException("Unknown node metric path " + path); + } + return metric; + } + + private static Map sortByPath() { + ImmutableMap.Builder result = ImmutableMap.builder(); + for (DefaultNodeMetric value : values()) { + result.put(value.getPath(), value); + } + return result.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metrics/DefaultSessionMetric.java b/core/src/main/java/com/datastax/oss/driver/api/core/metrics/DefaultSessionMetric.java new file mode 100644 index 00000000000..b9b01cf3364 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metrics/DefaultSessionMetric.java @@ -0,0 +1,65 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metrics; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; + +/** See {@code reference.conf} for a description of each metric. */ +public enum DefaultSessionMetric implements SessionMetric { + BYTES_SENT("bytes-sent"), + BYTES_RECEIVED("bytes-received"), + CONNECTED_NODES("connected-nodes"), + CQL_REQUESTS("cql-requests"), + CQL_CLIENT_TIMEOUTS("cql-client-timeouts"), + THROTTLING_DELAY("throttling.delay"), + THROTTLING_QUEUE_SIZE("throttling.queue-size"), + THROTTLING_ERRORS("throttling.errors"), + CQL_PREPARED_CACHE_SIZE("cql-prepared-cache-size"), + ; + + private static final Map BY_PATH = sortByPath(); + + private final String path; + + DefaultSessionMetric(String path) { + this.path = path; + } + + @NonNull + @Override + public String getPath() { + return path; + } + + @NonNull + public static DefaultSessionMetric fromPath(@NonNull String path) { + DefaultSessionMetric metric = BY_PATH.get(path); + if (metric == null) { + throw new IllegalArgumentException("Unknown session metric path " + path); + } + return metric; + } + + private static Map sortByPath() { + ImmutableMap.Builder result = ImmutableMap.builder(); + for (DefaultSessionMetric value : values()) { + result.put(value.getPath(), value); + } + return result.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metrics/Metrics.java b/core/src/main/java/com/datastax/oss/driver/api/core/metrics/Metrics.java new file mode 100644 index 00000000000..254be630a44 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metrics/Metrics.java @@ -0,0 +1,119 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metrics; + +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricRegistry; +import com.datastax.oss.driver.api.core.metadata.Node; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Optional; + +/** + * A wrapper around a {@link MetricRegistry} to expose the driver's metrics. + * + *

This type exists mainly to avoid a hard dependency to Dropwizard Metrics (that is, the JAR can + * be completely removed from the classpath if metrics are disabled). It also provides convenience + * methods to access individual metrics programatically. + */ +public interface Metrics { + + /** + * Returns the underlying Dropwizard registry. + * + *

Typically, this can be used to configure a reporter. + * + * @see Reporters + * (Dropwizard Metrics manual) + * @leaks-private-api + */ + @NonNull + MetricRegistry getRegistry(); + + /** + * Retrieves a session-level metric from the registry. + * + *

To determine the type of each metric, refer to the comments in the default {@code + * reference.conf} (included in the driver's codebase and JAR file). Note that the method does not + * check that this type is correct (there is no way to do this at runtime because some metrics are + * generic); if you use the wrong type, you will get a {@code ClassCastException} in your code: + * + *

{@code
+   * // Correct:
+   * Gauge connectedNodes = getNodeMetric(node, DefaultSessionMetric.CONNECTED_NODES);
+   *
+   * // Wrong, will throw CCE:
+   * Counter connectedNodes = getNodeMetric(node, DefaultSessionMetric.CONNECTED_NODES);
+   * }
+ * + * @param profileName the name of the execution profile, or {@code null} if the metric is not + * associated to any profile. Note that this is only included for future extensibility: at + * this time, the driver does not break up metrics per profile. Therefore you can always use + * {@link #getSessionMetric(SessionMetric)} instead of this method. + * @return the metric, or empty if it is disabled. + */ + @SuppressWarnings("TypeParameterUnusedInFormals") + @NonNull + Optional getSessionMetric( + @NonNull SessionMetric metric, @Nullable String profileName); + + /** + * Shortcut for {@link #getSessionMetric(SessionMetric, String) getSessionMetric(metric, null)}. + */ + @SuppressWarnings("TypeParameterUnusedInFormals") + @NonNull + default Optional getSessionMetric(@NonNull SessionMetric metric) { + return getSessionMetric(metric, null); + } + + /** + * Retrieves a node-level metric for a given node from the registry. + * + *

To determine the type of each metric, refer to the comments in the default {@code + * reference.conf} (included in the driver's codebase and JAR file). Note that the method does not + * check that this type is correct (there is no way to do this at runtime because some metrics are + * generic); if you use the wrong type, you will get a {@code ClassCastException} in your code: + * + *

{@code
+   * // Correct:
+   * Gauge openConnections = getNodeMetric(node, DefaultNodeMetric.OPEN_CONNECTIONS);
+   *
+   * // Wrong, will throw CCE:
+   * Counter openConnections = getNodeMetric(node, DefaultNodeMetric.OPEN_CONNECTIONS);
+   * }
+ * + * @param profileName the name of the execution profile, or {@code null} if the metric is not + * associated to any profile. Note that this is only included for future extensibility: at + * this time, the driver does not break up metrics per profile. Therefore you can always use + * {@link #getNodeMetric(Node, NodeMetric)} instead of this method. + * @return the metric, or empty if it is disabled. + */ + @SuppressWarnings("TypeParameterUnusedInFormals") + @NonNull + Optional getNodeMetric( + @NonNull Node node, @NonNull NodeMetric metric, @Nullable String profileName); + + /** + * Shortcut for {@link #getNodeMetric(Node, NodeMetric, String) getNodeMetric(node, metric, + * null)}. + */ + @SuppressWarnings("TypeParameterUnusedInFormals") + @NonNull + default Optional getNodeMetric( + @NonNull Node node, @NonNull NodeMetric metric) { + return getNodeMetric(node, metric, null); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metrics/NodeMetric.java b/core/src/main/java/com/datastax/oss/driver/api/core/metrics/NodeMetric.java new file mode 100644 index 00000000000..6f7c3c8e7f6 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metrics/NodeMetric.java @@ -0,0 +1,33 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metrics; + +import com.datastax.oss.driver.api.core.session.Session; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A node-level metric exposed through {@link Session#getMetrics()}. + * + *

All metrics exposed out of the box by the driver are instances of {@link DefaultNodeMetric} + * (this interface only exists to allow custom metrics in driver extensions). + * + * @see SessionMetric + */ +public interface NodeMetric { + + @NonNull + String getPath(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/metrics/SessionMetric.java b/core/src/main/java/com/datastax/oss/driver/api/core/metrics/SessionMetric.java new file mode 100644 index 00000000000..4b591e14085 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/metrics/SessionMetric.java @@ -0,0 +1,33 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.metrics; + +import com.datastax.oss.driver.api.core.session.Session; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A session-level metric exposed through {@link Session#getMetrics()}. + * + *

All metrics exposed out of the box by the driver are instances of {@link DefaultSessionMetric} + * (this interface only exists to allow custom metrics in driver extensions). + * + * @see NodeMetric + */ +public interface SessionMetric { + + @NonNull + String getPath(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/package-info.java b/core/src/main/java/com/datastax/oss/driver/api/core/package-info.java new file mode 100644 index 00000000000..919bdab6bd5 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** The core API of the driver, that deals with query execution and cluster metadata. */ +package com.datastax.oss.driver.api.core; diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/retry/RetryDecision.java b/core/src/main/java/com/datastax/oss/driver/api/core/retry/RetryDecision.java new file mode 100644 index 00000000000..8859cdd6e4f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/retry/RetryDecision.java @@ -0,0 +1,29 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.retry; + +/** A decision from the {@link RetryPolicy} on how to handle a retry. */ +public enum RetryDecision { + /** Retry the operation on the same node. */ + RETRY_SAME, + /** Retry the operation on the next available node in the query plan (if any). */ + RETRY_NEXT, + /** Rethrow to the calling code, as the result of the execute operation. */ + RETHROW, + /** Don't retry and return an empty result set to the calling code. */ + IGNORE, + ; +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/retry/RetryPolicy.java b/core/src/main/java/com/datastax/oss/driver/api/core/retry/RetryPolicy.java new file mode 100644 index 00000000000..e36658c9d8e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/retry/RetryPolicy.java @@ -0,0 +1,178 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.retry; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.connection.ClosedConnectionException; +import com.datastax.oss.driver.api.core.connection.HeartbeatException; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.servererrors.BootstrappingException; +import com.datastax.oss.driver.api.core.servererrors.CoordinatorException; +import com.datastax.oss.driver.api.core.servererrors.FunctionFailureException; +import com.datastax.oss.driver.api.core.servererrors.OverloadedException; +import com.datastax.oss.driver.api.core.servererrors.ProtocolError; +import com.datastax.oss.driver.api.core.servererrors.QueryValidationException; +import com.datastax.oss.driver.api.core.servererrors.ReadFailureException; +import com.datastax.oss.driver.api.core.servererrors.ReadTimeoutException; +import com.datastax.oss.driver.api.core.servererrors.ServerError; +import com.datastax.oss.driver.api.core.servererrors.TruncateException; +import com.datastax.oss.driver.api.core.servererrors.WriteFailureException; +import com.datastax.oss.driver.api.core.servererrors.WriteType; +import com.datastax.oss.driver.api.core.session.Request; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Defines the behavior to adopt when a request fails. + * + *

For each request, the driver gets a "query plan" (a list of coordinators to try) from the + * {@link LoadBalancingPolicy}, and tries each node in sequence. This policy is invoked if the + * request to that node fails. + * + *

The methods of this interface are invoked on I/O threads, therefore implementations should + * never block. In particular, don't call {@link Thread#sleep(long)} to retry after a delay: + * this would prevent asynchronous processing of other requests, and very negatively impact + * throughput. If the application needs to back off and retry later, this should be implemented in + * client code, not in this policy. + */ +public interface RetryPolicy extends AutoCloseable { + + /** + * Whether to retry when the server replied with a {@code READ_TIMEOUT} error; this indicates a + * server-side timeout during a read query, i.e. some replicas did not reply to the + * coordinator in time. + * + * @param request the request that timed out. + * @param cl the requested consistency level. + * @param blockFor the minimum number of replica acknowledgements/responses that were required to + * fulfill the operation. + * @param received the number of replica that had acknowledged/responded to the operation before + * it failed. + * @param dataPresent whether the actual data was amongst the received replica responses. See + * {@link ReadTimeoutException#wasDataPresent()}. + * @param retryCount how many times the retry policy has been invoked already for this request + * (not counting the current invocation). + */ + RetryDecision onReadTimeout( + @NonNull Request request, + @NonNull ConsistencyLevel cl, + int blockFor, + int received, + boolean dataPresent, + int retryCount); + + /** + * Whether to retry when the server replied with a {@code WRITE_TIMEOUT} error; this indicates a + * server-side timeout during a write query, i.e. some replicas did not reply to the + * coordinator in time. + * + *

Note that this method will only be invoked for {@link Request#isIdempotent()} idempotent} + * requests: when a write times out, it is impossible to determine with 100% certainty whether the + * mutation was applied or not, so the write is never safe to retry; the driver will rethrow the + * error directly, without invoking the retry policy. + * + * @param request the request that timed out. + * @param cl the requested consistency level. + * @param writeType the type of the write for which the timeout was raised. + * @param blockFor the minimum number of replica acknowledgements/responses that were required to + * fulfill the operation. + * @param received the number of replica that had acknowledged/responded to the operation before + * it failed. + * @param retryCount how many times the retry policy has been invoked already for this request + * (not counting the current invocation). + */ + RetryDecision onWriteTimeout( + @NonNull Request request, + @NonNull ConsistencyLevel cl, + @NonNull WriteType writeType, + int blockFor, + int received, + int retryCount); + + /** + * Whether to retry when the server replied with an {@code UNAVAILABLE} error; this indicates that + * the coordinator determined that there were not enough replicas alive to perform a query with + * the requested consistency level. + * + * @param request the request that timed out. + * @param cl the requested consistency level. + * @param required the number of replica acknowledgements/responses required to perform the + * operation (with its required consistency level). + * @param alive the number of replicas that were known to be alive by the coordinator node when it + * tried to execute the operation. + * @param retryCount how many times the retry policy has been invoked already for this request + * (not counting the current invocation). + */ + RetryDecision onUnavailable( + @NonNull Request request, + @NonNull ConsistencyLevel cl, + int required, + int alive, + int retryCount); + + /** + * Whether to retry when a request was aborted before we could get a response from the server. + * + *

This can happen in two cases: if the connection was closed due to an external event (this + * will manifest as a {@link ClosedConnectionException}, or {@link HeartbeatException} for a + * heartbeat failure); or if there was an unexpected error while decoding the response (this can + * only be a driver bug). + * + *

Note that this method will only be invoked for {@linkplain Request#isIdempotent() + * idempotent} requests: when execution was aborted before getting a response, it is impossible to + * determine with 100% certainty whether a mutation was applied or not, so a write is never safe + * to retry; the driver will rethrow the error directly, without invoking the retry policy. + * + * @param request the request that was aborted. + * @param error the error. + * @param retryCount how many times the retry policy has been invoked already for this request + * (not counting the current invocation). + */ + RetryDecision onRequestAborted( + @NonNull Request request, @NonNull Throwable error, int retryCount); + + /** + * Whether to retry when the server replied with a recoverable error (other than {@code + * READ_TIMEOUT}, {@code WRITE_TIMEOUT} or {@code UNAVAILABLE}). + * + *

This can happen for the following errors: {@link OverloadedException}, {@link ServerError}, + * {@link TruncateException}, {@link ReadFailureException}, {@link WriteFailureException}. + * + *

The following errors are handled internally by the driver, and therefore will never + * be encountered in this method: + * + *

    + *
  • {@link BootstrappingException}: always retried on the next node; + *
  • {@link QueryValidationException} (and its subclasses), {@link FunctionFailureException} + * and {@link ProtocolError}: always rethrown. + *
+ * + *

Note that this method will only be invoked for {@link Request#isIdempotent()} idempotent} + * requests: when execution was aborted before getting a response, it is impossible to determine + * with 100% certainty whether a mutation was applied or not, so a write is never safe to retry; + * the driver will rethrow the error directly, without invoking the retry policy. + * + * @param request the request that failed. + * @param error the error. + * @param retryCount how many times the retry policy has been invoked already for this request + * (not counting the current invocation). + */ + RetryDecision onErrorResponse( + @NonNull Request request, @NonNull CoordinatorException error, int retryCount); + + /** Called when the cluster that this policy is associated with closes. */ + @Override + void close(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/AlreadyExistsException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/AlreadyExistsException.java new file mode 100644 index 00000000000..7128c8c2d4f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/AlreadyExistsException.java @@ -0,0 +1,67 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Thrown when a query attempts to create a keyspace or table that already exists. + * + *

This exception does not go through the {@link RetryPolicy}, it is always rethrown directly to + * the client. + */ +public class AlreadyExistsException extends QueryValidationException { + + private final String keyspace; + private final String table; + + public AlreadyExistsException( + @NonNull Node coordinator, @NonNull String keyspace, @NonNull String table) { + this(coordinator, makeMessage(keyspace, table), keyspace, table, null, false); + } + + private AlreadyExistsException( + @NonNull Node coordinator, + @NonNull String message, + @NonNull String keyspace, + @NonNull String table, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + this.keyspace = keyspace; + this.table = table; + } + + private static String makeMessage(String keyspace, String table) { + if (table == null || table.isEmpty()) { + return String.format("Keyspace %s already exists", keyspace); + } else { + return String.format("Object %s.%s already exists", keyspace, table); + } + } + + @NonNull + @Override + public DriverException copy() { + return new AlreadyExistsException( + getCoordinator(), getMessage(), keyspace, table, getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/BootstrappingException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/BootstrappingException.java new file mode 100644 index 00000000000..bd96aff1518 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/BootstrappingException.java @@ -0,0 +1,52 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Thrown when the coordinator was bootstrapping when it received a query. + * + *

This exception does not go through the {@link RetryPolicy}, the query is always retried on the + * next node. Therefore the only way the client can observe this exception is in an {@link + * AllNodesFailedException}. + */ +public class BootstrappingException extends QueryExecutionException { + + public BootstrappingException(@NonNull Node coordinator) { + this(coordinator, String.format("%s is bootstrapping", coordinator), null, false); + } + + private BootstrappingException( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } + + @NonNull + @Override + public DriverException copy() { + return new BootstrappingException(getCoordinator(), getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/CoordinatorException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/CoordinatorException.java new file mode 100644 index 00000000000..975d7252747 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/CoordinatorException.java @@ -0,0 +1,45 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** A server-side error thrown by the coordinator node in response to a driver request. */ +public abstract class CoordinatorException extends DriverException { + + // This is also present on ExecutionInfo. But the execution info is only set for errors that are + // rethrown to the client, not on errors that get retried. It can be useful to know the node in + // the retry policy, so store it here, it might be duplicated but that doesn't matter. + private final Node coordinator; + + protected CoordinatorException( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(message, executionInfo, null, writableStackTrace); + this.coordinator = coordinator; + } + + @NonNull + public Node getCoordinator() { + return coordinator; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/DefaultWriteType.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/DefaultWriteType.java new file mode 100644 index 00000000000..b7fe225ff61 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/DefaultWriteType.java @@ -0,0 +1,60 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +/** A default write type supported by the driver out of the box. */ +public enum DefaultWriteType implements WriteType { + + /** A write to a single partition key. Such writes are guaranteed to be atomic and isolated. */ + SIMPLE, + /** + * A write to a multiple partition key that used the distributed batch log to ensure atomicity + * (atomicity meaning that if any statement in the batch succeeds, all will eventually succeed). + */ + BATCH, + /** + * A write to a multiple partition key that doesn't use the distributed batch log. Atomicity for + * such writes is not guaranteed + */ + UNLOGGED_BATCH, + /** + * A counter write (that can be for one or multiple partition key). Such write should not be + * replayed to avoid over-counting. + */ + COUNTER, + /** + * The initial write to the distributed batch log that Cassandra performs internally before a + * BATCH write. + */ + BATCH_LOG, + /** + * A conditional write. If a timeout has this {@code WriteType}, the timeout has happened while + * doing the compare-and-swap for an conditional update. In this case, the update may or may not + * have been applied. + */ + CAS, + /** + * Indicates that the timeout was related to acquiring locks needed for updating materialized + * views affected by write operation. + */ + VIEW, + /** + * Indicates that the timeout was related to acquiring space for change data capture logs for cdc + * tracked tables. + */ + CDC, + ; +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/FunctionFailureException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/FunctionFailureException.java new file mode 100644 index 00000000000..1a9b178d0fe --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/FunctionFailureException.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * An error during the execution of a CQL function. + * + *

This exception does not go through the {@link RetryPolicy}, it is always rethrown directly to + * the client. + */ +public class FunctionFailureException extends QueryExecutionException { + + public FunctionFailureException(@NonNull Node coordinator, @NonNull String message) { + this(coordinator, message, null, false); + } + + private FunctionFailureException( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } + + @NonNull + @Override + public DriverException copy() { + return new FunctionFailureException(getCoordinator(), getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/InvalidConfigurationInQueryException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/InvalidConfigurationInQueryException.java new file mode 100644 index 00000000000..900d9c281ed --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/InvalidConfigurationInQueryException.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Indicates that a query is invalid because of some configuration problem. + * + *

This is generally throw by queries that manipulate the schema (CREATE and ALTER) when the + * required configuration options are invalid. + * + *

This exception does not go through the {@link RetryPolicy}, it is always rethrown directly to + * the client. + */ +public class InvalidConfigurationInQueryException extends QueryValidationException { + + public InvalidConfigurationInQueryException(@NonNull Node coordinator, @NonNull String message) { + this(coordinator, message, null, false); + } + + private InvalidConfigurationInQueryException( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } + + @NonNull + @Override + public DriverException copy() { + return new InvalidConfigurationInQueryException( + getCoordinator(), getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/InvalidQueryException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/InvalidQueryException.java new file mode 100644 index 00000000000..bbf50a5b5fb --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/InvalidQueryException.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Indicates a syntactically correct, but invalid query. + * + *

This exception does not go through the {@link RetryPolicy}, it is always rethrown directly to + * the client. + */ +public class InvalidQueryException extends QueryValidationException { + + public InvalidQueryException(@NonNull Node coordinator, @NonNull String message) { + this(coordinator, message, null, false); + } + + private InvalidQueryException( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } + + @NonNull + @Override + public DriverException copy() { + return new InvalidQueryException(getCoordinator(), getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/OverloadedException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/OverloadedException.java new file mode 100644 index 00000000000..4b7b4bb6d9a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/OverloadedException.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Thrown when the coordinator reported itself as being overloaded. + * + *

This exception is processed by {@link RetryPolicy#onErrorResponse(Request, + * CoordinatorException, int)}, which will decide if it is rethrown directly to the client or if the + * request should be retried. If all other tried nodes also fail, this exception will appear in the + * {@link AllNodesFailedException} thrown to the client. + */ +public class OverloadedException extends QueryExecutionException { + + public OverloadedException(@NonNull Node coordinator) { + super(coordinator, String.format("%s is bootstrapping", coordinator), null, false); + } + + private OverloadedException( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } + + @NonNull + @Override + public DriverException copy() { + return new OverloadedException(getCoordinator(), getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ProtocolError.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ProtocolError.java new file mode 100644 index 00000000000..813bfa6cae9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ProtocolError.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Indicates that the contacted node reported a protocol error. + * + *

Protocol errors indicate that the client triggered a protocol violation (for instance, a + * {@code QUERY} message is sent before a {@code STARTUP} one has been sent). Protocol errors should + * be considered as a bug in the driver and reported as such. + * + *

This exception does not go through the {@link RetryPolicy}, it is always rethrown directly to + * the client. + */ +public class ProtocolError extends CoordinatorException { + + public ProtocolError(@NonNull Node coordinator, @NonNull String message) { + this(coordinator, message, null, false); + } + + private ProtocolError( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } + + @NonNull + @Override + public DriverException copy() { + return new ProtocolError(getCoordinator(), getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/QueryConsistencyException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/QueryConsistencyException.java new file mode 100644 index 00000000000..67c4fcd9ca0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/QueryConsistencyException.java @@ -0,0 +1,73 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A failure to reach the required consistency level during the execution of a query. + * + *

Such an exception is returned when the query has been tried by Cassandra but cannot be + * achieved with the requested consistency level because either: + * + *

    + *
  • the coordinator did not receive enough replica responses within the rpc timeout set for + * Cassandra; + *
  • some replicas replied with an error. + *
+ */ +public abstract class QueryConsistencyException extends QueryExecutionException { + + private final ConsistencyLevel consistencyLevel; + private final int received; + private final int blockFor; + + protected QueryConsistencyException( + @NonNull Node coordinator, + @NonNull String message, + @NonNull ConsistencyLevel consistencyLevel, + int received, + int blockFor, + ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + this.consistencyLevel = consistencyLevel; + this.received = received; + this.blockFor = blockFor; + } + + /** The consistency level of the operation that failed. */ + @NonNull + public ConsistencyLevel getConsistencyLevel() { + return consistencyLevel; + } + + /** The number of replica that had acknowledged/responded to the operation before it failed. */ + public int getReceived() { + return received; + } + + /** + * The minimum number of replica acknowledgements/responses that were required to fulfill the + * operation. + */ + public int getBlockFor() { + return blockFor; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/QueryExecutionException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/QueryExecutionException.java new file mode 100644 index 00000000000..a23d9d9dca7 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/QueryExecutionException.java @@ -0,0 +1,33 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** A server-side error thrown when a valid query cannot be executed. */ +public abstract class QueryExecutionException extends CoordinatorException { + + protected QueryExecutionException( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/QueryValidationException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/QueryValidationException.java new file mode 100644 index 00000000000..448a6026892 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/QueryValidationException.java @@ -0,0 +1,36 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * A server-side error thrown when a query cannot be executed because it is syntactically incorrect, + * invalid or unauthorized. + */ +public abstract class QueryValidationException extends CoordinatorException { + + protected QueryValidationException( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ReadFailureException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ReadFailureException.java new file mode 100644 index 00000000000..adecf20ccbe --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ReadFailureException.java @@ -0,0 +1,151 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.net.InetAddress; +import java.util.Map; + +/** + * A non-timeout error during a read query. + * + *

This happens when some of the replicas that were contacted by the coordinator replied with an + * error. + * + *

This exception is processed by {@link RetryPolicy#onErrorResponse(Request, + * CoordinatorException, int)}, which will decide if it is rethrown directly to the client or if the + * request should be retried. If all other tried nodes also fail, this exception will appear in the + * {@link AllNodesFailedException} thrown to the client. + */ +public class ReadFailureException extends QueryConsistencyException { + + private final int numFailures; + private final boolean dataPresent; + private final Map reasonMap; + + public ReadFailureException( + @NonNull Node coordinator, + @NonNull ConsistencyLevel consistencyLevel, + int received, + int blockFor, + int numFailures, + boolean dataPresent, + @NonNull Map reasonMap) { + this( + coordinator, + String.format( + "Cassandra failure during read query at consistency %s " + + "(%d responses were required but only %d replica responded, %d failed)", + consistencyLevel, blockFor, received, numFailures), + consistencyLevel, + received, + blockFor, + numFailures, + dataPresent, + reasonMap, + null, + false); + } + + private ReadFailureException( + @NonNull Node coordinator, + @NonNull String message, + @NonNull ConsistencyLevel consistencyLevel, + int received, + int blockFor, + int numFailures, + boolean dataPresent, + @NonNull Map reasonMap, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super( + coordinator, + message, + consistencyLevel, + received, + blockFor, + executionInfo, + writableStackTrace); + this.numFailures = numFailures; + this.dataPresent = dataPresent; + this.reasonMap = reasonMap; + } + + /** Returns the number of replicas that experienced a failure while executing the request. */ + public int getNumFailures() { + return numFailures; + } + + /** + * Whether the actual data was amongst the received replica responses. + * + *

During reads, Cassandra doesn't request data from every replica to minimize internal network + * traffic. Instead, some replicas are only asked for a checksum of the data. A read failure may + * occur even if enough replicas have responded to fulfill the consistency level, if only checksum + * responses have been received. This method allows to detect that case. + */ + public boolean wasDataPresent() { + return dataPresent; + } + + /** + * Returns the a failure reason code for each node that failed. + * + *

At the time of writing, the existing reason codes are: + * + *

    + *
  • {@code 0x0000}: the error does not have a specific code assigned yet, or the cause is + * unknown. + *
  • {@code 0x0001}: The read operation scanned too many tombstones (as defined by {@code + * tombstone_failure_threshold} in {@code cassandra.yaml}, causing a {@code + * TombstoneOverwhelmingException}. + *
+ * + * (please refer to the Cassandra documentation for your version for the most up-to-date list of + * errors) + * + *

This feature is available for protocol v5 or above only. With lower protocol versions, the + * map will always be empty. + */ + @NonNull + public Map getReasonMap() { + return reasonMap; + } + + @NonNull + @Override + public DriverException copy() { + return new ReadFailureException( + getCoordinator(), + getMessage(), + getConsistencyLevel(), + getReceived(), + getBlockFor(), + numFailures, + dataPresent, + reasonMap, + getExecutionInfo(), + true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ReadTimeoutException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ReadTimeoutException.java new file mode 100644 index 00000000000..1d199a695eb --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ReadTimeoutException.java @@ -0,0 +1,114 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A server-side timeout during a read query. + * + *

This exception is processed by {@link RetryPolicy#onReadTimeout(Request, ConsistencyLevel, + * int, int, boolean, int)}, which will decide if it is rethrown directly to the client or if the + * request should be retried. If all other tried nodes also fail, this exception will appear in the + * {@link AllNodesFailedException} thrown to the client. + */ +public class ReadTimeoutException extends QueryConsistencyException { + + private final boolean dataPresent; + + public ReadTimeoutException( + @NonNull Node coordinator, + @NonNull ConsistencyLevel consistencyLevel, + int received, + int blockFor, + boolean dataPresent) { + this( + coordinator, + String.format( + "Cassandra timeout during read query at consistency %s (%s)", + consistencyLevel, formatDetails(received, blockFor, dataPresent)), + consistencyLevel, + received, + blockFor, + dataPresent, + null, + false); + } + + private ReadTimeoutException( + @NonNull Node coordinator, + @NonNull String message, + @NonNull ConsistencyLevel consistencyLevel, + int received, + int blockFor, + boolean dataPresent, + ExecutionInfo executionInfo, + boolean writableStackTrace) { + super( + coordinator, + message, + consistencyLevel, + received, + blockFor, + executionInfo, + writableStackTrace); + this.dataPresent = dataPresent; + } + + private static String formatDetails(int received, int blockFor, boolean dataPresent) { + if (received < blockFor) { + return String.format( + "%d responses were required but only %d replica responded", blockFor, received); + } else if (!dataPresent) { + return "the replica queried for data didn't respond"; + } else { + return "timeout while waiting for repair of inconsistent replica"; + } + } + + /** + * Whether the actual data was amongst the received replica responses. + * + *

During reads, Cassandra doesn't request data from every replica to minimize internal network + * traffic. Instead, some replicas are only asked for a checksum of the data. A read timeout may + * occur even if enough replicas have responded to fulfill the consistency level, if only checksum + * responses have been received. This method allows to detect that case. + */ + public boolean wasDataPresent() { + return dataPresent; + } + + @NonNull + @Override + public DriverException copy() { + return new ReadTimeoutException( + getCoordinator(), + getMessage(), + getConsistencyLevel(), + getReceived(), + getBlockFor(), + dataPresent, + getExecutionInfo(), + true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ServerError.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ServerError.java new file mode 100644 index 00000000000..9afe5ea45b3 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/ServerError.java @@ -0,0 +1,56 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Indicates that the contacted node reported an internal error. + * + *

This should be considered as a server bug and reported as such. + * + *

This exception is processed by {@link RetryPolicy#onErrorResponse(Request, + * CoordinatorException, int)}, which will decide if it is rethrown directly to the client or if the + * request should be retried. If all other tried nodes also fail, this exception will appear in the + * {@link AllNodesFailedException} thrown to the client. + */ +public class ServerError extends CoordinatorException { + + public ServerError(@NonNull Node coordinator, @NonNull String message) { + this(coordinator, message, null, false); + } + + private ServerError( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } + + @NonNull + @Override + public DriverException copy() { + return new ServerError(getCoordinator(), getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/SyntaxError.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/SyntaxError.java new file mode 100644 index 00000000000..32c5e930741 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/SyntaxError.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * A syntax error in a query. + * + *

This exception does not go through the {@link RetryPolicy}, it is always rethrown directly to + * the client. + */ +public class SyntaxError extends QueryValidationException { + + public SyntaxError(@NonNull Node coordinator, @NonNull String message) { + this(coordinator, message, null, false); + } + + private SyntaxError( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } + + @NonNull + @Override + public DriverException copy() { + return new SyntaxError(getCoordinator(), getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/TruncateException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/TruncateException.java new file mode 100644 index 00000000000..12f265e135d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/TruncateException.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * An error during a truncation operation. + * + *

This exception is processed by {@link RetryPolicy#onErrorResponse(Request, + * CoordinatorException, int)}, which will decide if it is rethrown directly to the client or if the + * request should be retried. If all other tried nodes also fail, this exception will appear in the + * {@link AllNodesFailedException} thrown to the client. + */ +public class TruncateException extends QueryExecutionException { + + public TruncateException(@NonNull Node coordinator, @NonNull String message) { + this(coordinator, message, null, false); + } + + private TruncateException( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } + + @NonNull + @Override + public DriverException copy() { + return new TruncateException(getCoordinator(), getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/UnauthorizedException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/UnauthorizedException.java new file mode 100644 index 00000000000..e15ab02dc4b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/UnauthorizedException.java @@ -0,0 +1,51 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Indicates that a query cannot be performed due to the authorization restrictions of the logged + * user. + * + *

This exception does not go through the {@link RetryPolicy}, it is always rethrown directly to + * the client. + */ +public class UnauthorizedException extends QueryValidationException { + + public UnauthorizedException(@NonNull Node coordinator, @NonNull String message) { + this(coordinator, message, null, false); + } + + private UnauthorizedException( + @NonNull Node coordinator, + @NonNull String message, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + } + + @NonNull + @Override + public DriverException copy() { + return new UnauthorizedException(getCoordinator(), getMessage(), getExecutionInfo(), true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/UnavailableException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/UnavailableException.java new file mode 100644 index 00000000000..98e119791d8 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/UnavailableException.java @@ -0,0 +1,106 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Thrown when the coordinator knows there is not enough replicas alive to perform a query with the + * requested consistency level. + * + *

This exception is processed by {@link RetryPolicy#onUnavailable(Request, ConsistencyLevel, + * int, int, int)}, which will decide if it is rethrown directly to the client or if the request + * should be retried. If all other tried nodes also fail, this exception will appear in the {@link + * AllNodesFailedException} thrown to the client. + */ +public class UnavailableException extends QueryExecutionException { + private final ConsistencyLevel consistencyLevel; + private final int required; + private final int alive; + + public UnavailableException( + @NonNull Node coordinator, + @NonNull ConsistencyLevel consistencyLevel, + int required, + int alive) { + this( + coordinator, + String.format( + "Not enough replicas available for query at consistency %s (%d required but only %d alive)", + consistencyLevel, required, alive), + consistencyLevel, + required, + alive, + null, + false); + } + + private UnavailableException( + @NonNull Node coordinator, + @NonNull String message, + @NonNull ConsistencyLevel consistencyLevel, + int required, + int alive, + ExecutionInfo executionInfo, + boolean writableStackTrace) { + super(coordinator, message, executionInfo, writableStackTrace); + this.consistencyLevel = consistencyLevel; + this.required = required; + this.alive = alive; + } + + /** The consistency level of the operation triggering this exception. */ + @NonNull + public ConsistencyLevel getConsistencyLevel() { + return consistencyLevel; + } + + /** + * The number of replica acknowledgements/responses required to perform the operation (with its + * required consistency level). + */ + public int getRequired() { + return required; + } + + /** + * The number of replicas that were known to be alive by the coordinator node when it tried to + * execute the operation. + */ + public int getAlive() { + return alive; + } + + @NonNull + @Override + public DriverException copy() { + return new UnavailableException( + getCoordinator(), + getMessage(), + consistencyLevel, + required, + alive, + getExecutionInfo(), + true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/WriteFailureException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/WriteFailureException.java new file mode 100644 index 00000000000..f2589ff1b65 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/WriteFailureException.java @@ -0,0 +1,145 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.net.InetAddress; +import java.util.Map; + +/** + * A non-timeout error during a write query. + * + *

This happens when some of the replicas that were contacted by the coordinator replied with an + * error. + * + *

This exception is processed by {@link RetryPolicy#onErrorResponse(Request, + * CoordinatorException, int)}, which will decide if it is rethrown directly to the client or if the + * request should be retried. If all other tried nodes also fail, this exception will appear in the + * {@link AllNodesFailedException} thrown to the client. + */ +public class WriteFailureException extends QueryConsistencyException { + + private final WriteType writeType; + private final int numFailures; + private final Map reasonMap; + + public WriteFailureException( + @NonNull Node coordinator, + @NonNull ConsistencyLevel consistencyLevel, + int received, + int blockFor, + @NonNull WriteType writeType, + int numFailures, + @NonNull Map reasonMap) { + this( + coordinator, + String.format( + "Cassandra failure during write query at consistency %s " + + "(%d responses were required but only %d replica responded, %d failed)", + consistencyLevel, blockFor, received, numFailures), + consistencyLevel, + received, + blockFor, + writeType, + numFailures, + reasonMap, + null, + false); + } + + private WriteFailureException( + @NonNull Node coordinator, + @NonNull String message, + @NonNull ConsistencyLevel consistencyLevel, + int received, + int blockFor, + @NonNull WriteType writeType, + int numFailures, + @NonNull Map reasonMap, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super( + coordinator, + message, + consistencyLevel, + received, + blockFor, + executionInfo, + writableStackTrace); + this.writeType = writeType; + this.numFailures = numFailures; + this.reasonMap = reasonMap; + } + + /** The type of the write for which this failure was raised. */ + @NonNull + public WriteType getWriteType() { + return writeType; + } + + /** Returns the number of replicas that experienced a failure while executing the request. */ + public int getNumFailures() { + return numFailures; + } + + /** + * Returns the a failure reason code for each node that failed. + * + *

At the time of writing, the existing reason codes are: + * + *

    + *
  • {@code 0x0000}: the error does not have a specific code assigned yet, or the cause is + * unknown. + *
  • {@code 0x0001}: The read operation scanned too many tombstones (as defined by {@code + * tombstone_failure_threshold} in {@code cassandra.yaml}, causing a {@code + * TombstoneOverwhelmingException}. + *
+ * + * (please refer to the Cassandra documentation for your version for the most up-to-date list of + * errors) + * + *

This feature is available for protocol v5 or above only. With lower protocol versions, the + * map will always be empty. + */ + @NonNull + public Map getReasonMap() { + return reasonMap; + } + + @NonNull + @Override + public DriverException copy() { + return new WriteFailureException( + getCoordinator(), + getMessage(), + getConsistencyLevel(), + getReceived(), + getBlockFor(), + writeType, + numFailures, + reasonMap, + getExecutionInfo(), + true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/WriteTimeoutException.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/WriteTimeoutException.java new file mode 100644 index 00000000000..600b5e36895 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/WriteTimeoutException.java @@ -0,0 +1,99 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * A server-side timeout during a write query. + * + *

This exception is processed by {@link RetryPolicy#onWriteTimeout(Request, ConsistencyLevel, + * WriteType, int, int, int)}, which will decide if it is rethrown directly to the client or if the + * request should be retried. If all other tried nodes also fail, this exception will appear in the + * {@link AllNodesFailedException} thrown to the client. + */ +public class WriteTimeoutException extends QueryConsistencyException { + + private final WriteType writeType; + + public WriteTimeoutException( + @NonNull Node coordinator, + @NonNull ConsistencyLevel consistencyLevel, + int received, + int blockFor, + @NonNull WriteType writeType) { + this( + coordinator, + String.format( + "Cassandra timeout during %s write query at consistency %s " + + "(%d replica were required but only %d acknowledged the write)", + writeType, consistencyLevel, blockFor, received), + consistencyLevel, + received, + blockFor, + writeType, + null, + false); + } + + private WriteTimeoutException( + @NonNull Node coordinator, + @NonNull String message, + @NonNull ConsistencyLevel consistencyLevel, + int received, + int blockFor, + @NonNull WriteType writeType, + @Nullable ExecutionInfo executionInfo, + boolean writableStackTrace) { + super( + coordinator, + message, + consistencyLevel, + received, + blockFor, + executionInfo, + writableStackTrace); + this.writeType = writeType; + } + + /** The type of the write for which a timeout was raised. */ + @NonNull + public WriteType getWriteType() { + return writeType; + } + + @NonNull + @Override + public DriverException copy() { + return new WriteTimeoutException( + getCoordinator(), + getMessage(), + getConsistencyLevel(), + getReceived(), + getBlockFor(), + writeType, + getExecutionInfo(), + true); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/WriteType.java b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/WriteType.java new file mode 100644 index 00000000000..e34d90ad78b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/servererrors/WriteType.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.servererrors; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The type of a Cassandra write query. + * + *

This information is returned by Cassandra when a write timeout is raised, to indicate what + * type of write timed out. It is useful to decide which retry decision to adopt. + * + *

The only reason to model this as an interface (as opposed to an enum type) is to accommodate + * for custom protocol extensions. If you're connecting to a standard Apache Cassandra cluster, all + * {@code WriteType}s are {@link DefaultWriteType} instances. + */ +public interface WriteType { + + /** The textual representation that the write type is encoded to in protocol frames. */ + @NonNull + String name(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/session/Request.java b/core/src/main/java/com/datastax/oss/driver/api/core/session/Request.java new file mode 100644 index 00000000000..b9645fc7e43 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/session/Request.java @@ -0,0 +1,168 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.session; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Map; + +/** + * A request executed by a {@link Session}. + * + *

This is a high-level abstraction, agnostic to the actual language (e.g. CQL). A request is + * anything that can be converted to a protocol message, provided that you register a request + * processor with the driver to do that conversion. + */ +public interface Request { + + /** + * The name of the execution profile that will be used for this request, or {@code null} if no + * profile has been set. + * + *

Note that this will be ignored if {@link #getExecutionProfile()} returns a non-null value. + * + * @see DriverConfig + */ + @Nullable + String getExecutionProfileName(); + + /** + * The execution profile to use for this request, or {@code null} if no profile has been set. + * + *

It is generally simpler to specify a profile name with {@link #getExecutionProfileName()}. + * However, this method can be used to provide a "derived" profile that was built programmatically + * by the client code. If specified, it overrides the profile name. + * + * @see DriverExecutionProfile + */ + @Nullable + DriverExecutionProfile getExecutionProfile(); + + /** + * The CQL keyspace to execute this request in, or {@code null} if this request does not specify + * any keyspace. + * + *

This overrides {@link Session#getKeyspace()} for this particular request, providing a way to + * specify the keyspace without forcing it globally on the session, nor hard-coding it in the + * query string. + * + *

This feature is only available with {@link DefaultProtocolVersion#V5 native protocol v5} or + * higher. Specifying a per-request keyspace with lower protocol versions will cause a runtime + * error. + * + * @see CASSANDRA-10145 + */ + @Nullable + CqlIdentifier getKeyspace(); + + /** + * The keyspace to use for token-aware routing, if no {@link #getKeyspace() per-request keyspace} + * is defined, or {@code null} if this request does not use token-aware routing. + * + *

See {@link #getRoutingKey()} for a detailed explanation of token-aware routing. + * + *

Note that this is the only way to define a routing keyspace for protocol v4 or lower. + */ + @Nullable + CqlIdentifier getRoutingKeyspace(); + + /** + * The (encoded) partition key to use for token-aware routing, or {@code null} if this request + * does not use token-aware routing. + * + *

When the driver picks a coordinator to execute a request, it prioritizes the replicas of the + * partition that this query operates on, in order to avoid an extra network jump on the server + * side. To find these replicas, it needs a keyspace (which is where the replication settings are + * defined) and a key, that are computed the following way: + * + *

    + *
  • if a per-request keyspace is specified with {@link #getKeyspace()}, it is used as the + * keyspace; + *
  • otherwise, if {@link #getRoutingKeyspace()} is specified, it is used as the keyspace; + *
  • otherwise, if {@link Session#getKeyspace()} is not {@code null}, it is used as the + * keyspace; + *
  • if a routing token is defined with {@link #getRoutingToken()}, it is used as the key; + *
  • otherwise, the result of this method is used as the key. + *
+ * + * If either keyspace or key is {@code null} at the end of this process, then token-aware routing + * is disabled. + */ + @Nullable + ByteBuffer getRoutingKey(); + + /** + * The token to use for token-aware routing, or {@code null} if this request does not use + * token-aware routing. + * + *

This is the same information as {@link #getRoutingKey()}, but already hashed in a token. It + * is probably more useful for analytics tools that "shard" a query on a set of token ranges. + * + *

See {@link #getRoutingKey()} for a detailed explanation of token-aware routing. + */ + @Nullable + Token getRoutingToken(); + + /** + * Returns the custom payload to send alongside the request. + * + *

This is used to exchange extra information with the server. By default, Cassandra doesn't do + * anything with this, you'll only need it if you have a custom request handler on the + * server-side. + * + * @return The custom payload, or an empty map if no payload is present. + */ + @NonNull + Map getCustomPayload(); + + /** + * Whether the request is idempotent; that is, whether applying the request twice leaves the + * database in the same state. + * + *

This is used internally for retries and speculative executions: if a request is not + * idempotent, the driver will take extra care to ensure that it is not sent twice (for example, + * don't retry if there is the slightest chance that the request reached a coordinator). + * + * @return a boolean value, or {@code null} to use the default value defined in the configuration. + * @see DefaultDriverOption#REQUEST_DEFAULT_IDEMPOTENCE + */ + @Nullable + Boolean isIdempotent(); + + /** + * How long to wait for this request to complete. This is a global limit on the duration of a + * session.execute() call, including any retries the driver might do. + * + * @return the set duration, or {@code null} to use the default value defined in the + * configuration. + * @see DefaultDriverOption#REQUEST_TIMEOUT + */ + @Nullable + Duration getTimeout(); + + /** @return The node configured on this statement, or null if none is configured. */ + @Nullable + Node getNode(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/session/Session.java b/core/src/main/java/com/datastax/oss/driver/api/core/session/Session.java new file mode 100644 index 00000000000..65c49988f0c --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/session/Session.java @@ -0,0 +1,212 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.session; + +import com.datastax.oss.driver.api.core.AsyncAutoCloseable; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import com.datastax.oss.driver.api.core.MavenCoordinates; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.metrics.Metrics; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.DefaultMavenCoordinates; +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +/** + * A nexus to send requests to a Cassandra cluster. + * + *

This is a high-level abstraction capable of handling arbitrary request and result types. For + * CQL statements, {@link CqlSession} provides convenience methods with more familiar signatures (by + * default, all the instances returned by the driver also implement {@link CqlSession}). + * + *

The driver's request execution logic is pluggable (see {@code RequestProcessor} in the + * internal API). This is intended for future extensions, for example a reactive API for CQL + * statements, or graph requests in the Datastax Enterprise driver. Hence the generic {@link + * #execute(Request, GenericType)} method in this interface, that makes no assumptions about the + * request or result type. + * + * @see CqlSession#builder() + */ +public interface Session extends AsyncAutoCloseable { + + /** + * The Maven coordinates of the core driver artifact. + * + *

This is intended for products that wrap or extend the driver, as a way to check + * compatibility if end-users override the driver version in their application. + */ + @NonNull + MavenCoordinates OSS_DRIVER_COORDINATES = + DefaultMavenCoordinates.buildFromResourceAndPrint( + Session.class.getResource("/com/datastax/oss/driver/Driver.properties")); + + /** + * The unique name identifying this client. + * + * @see DefaultDriverOption#SESSION_NAME + */ + @NonNull + String getName(); + + /** + * Returns a snapshot of the Cassandra cluster's topology and schema metadata. + * + *

In order to provide atomic updates, this method returns an immutable object: the node list, + * token map, and schema contained in a given instance will always be consistent with each other + * (but note that {@link Node} itself is not immutable: some of its properties will be updated + * dynamically, in particular {@link Node#getState()}). + * + *

As a consequence of the above, you should call this method each time you need a fresh view + * of the metadata. Do not call it once and store the result, because it is a frozen + * snapshot that will become stale over time. + * + *

If a metadata refresh triggers events (such as node added/removed, or schema events), then + * the new version of the metadata is guaranteed to be visible by the time you receive these + * events. + * + *

The returned object is never {@code null}, but may be empty if metadata has been disabled in + * the configuration. + */ + @NonNull + Metadata getMetadata(); + + /** Whether schema metadata is currently enabled. */ + boolean isSchemaMetadataEnabled(); + + /** + * Enable or disable schema metadata programmatically. + * + *

Use this method to override the value defined in the driver's configuration; one typical use + * case is to temporarily disable schema metadata while the client issues a sequence of DDL + * statements. + * + *

If calling this method re-enables the metadata (that is, {@link #isSchemaMetadataEnabled()} + * was false before, and becomes true as a result of the call), a refresh is also triggered. + * + * @param newValue a boolean value to enable or disable schema metadata programmatically, or + * {@code null} to use the driver's configuration. + * @see DefaultDriverOption#METADATA_SCHEMA_ENABLED + * @return if this call triggered a refresh, a future that will complete when that refresh is + * complete. Otherwise, a completed future with the current metadata. + */ + @NonNull + CompletionStage setSchemaMetadataEnabled(@Nullable Boolean newValue); + + /** + * Force an immediate refresh of the schema metadata, even if it is currently disabled (either in + * the configuration or via {@link #setSchemaMetadataEnabled(Boolean)}). + * + *

The new metadata is returned in the resulting future (and will also be reflected by {@link + * #getMetadata()} when that future completes). + */ + @NonNull + CompletionStage refreshSchemaAsync(); + + /** + * Convenience method to call {@link #refreshSchemaAsync()} and block for the result. + * + *

This must not be called on a driver thread. + */ + @NonNull + default Metadata refreshSchema() { + BlockingOperation.checkNotDriverThread(); + return CompletableFutures.getUninterruptibly(refreshSchemaAsync()); + } + + /** + * Checks if all nodes in the cluster agree on a common schema version. + * + *

Due to the distributed nature of Cassandra, schema changes made on one node might not be + * immediately visible to others. Under certain circumstances, the driver waits until all nodes + * agree on a common schema version (namely: before a schema refresh, and before completing a + * successful schema-altering query). To do so, it queries system tables to find out the schema + * version of all nodes that are currently {@link NodeState#UP UP}. If all the versions match, the + * check succeeds, otherwise it is retried periodically, until a given timeout (specified in the + * configuration). + * + *

A schema agreement failure is not fatal, but it might produce unexpected results (for + * example, getting an "unconfigured table" error for a table that you created right before, just + * because the two queries went to different coordinators). + * + *

Note that schema agreement never succeeds in a mixed-version cluster (it would be + * challenging because the way the schema version is computed varies across server versions); the + * assumption is that schema updates are unlikely to happen during a rolling upgrade anyway. + * + * @return a future that completes with {@code true} if the nodes agree, or {@code false} if the + * timeout fired. + * @see DefaultDriverOption#CONTROL_CONNECTION_AGREEMENT_INTERVAL + * @see DefaultDriverOption#CONTROL_CONNECTION_AGREEMENT_TIMEOUT + */ + @NonNull + CompletionStage checkSchemaAgreementAsync(); + + /** + * Convenience method to call {@link #checkSchemaAgreementAsync()} and block for the result. + * + *

This must not be called on a driver thread. + */ + default boolean checkSchemaAgreement() { + BlockingOperation.checkNotDriverThread(); + return CompletableFutures.getUninterruptibly(checkSchemaAgreementAsync()); + } + + /** Returns a context that provides access to all the policies used by this driver instance. */ + @NonNull + DriverContext getContext(); + + /** + * The keyspace that this session is currently connected to, or {@link Optional#empty()} if this + * session is not connected to any keyspace. + * + *

There are two ways that this can be set: before initializing the session (either with the + * {@code session-keyspace} option in the configuration, or with {@link + * CqlSessionBuilder#withKeyspace(CqlIdentifier)}); or at runtime, if the client issues a request + * that changes the keyspace (such as a CQL {@code USE} query). Note that this second method is + * inherently unsafe, since other requests expecting the old keyspace might be executing + * concurrently. Therefore it is highly discouraged, aside from trivial cases (such as a + * cqlsh-style program where requests are never concurrent). + */ + @NonNull + Optional getKeyspace(); + + /** + * Returns a gateway to the driver's metrics, or {@link Optional#empty()} if all metrics are + * disabled. + */ + @NonNull + Optional getMetrics(); + + /** + * Executes an arbitrary request. + * + * @param resultType the type of the result, which determines the internal request processor + * (built-in or custom) that will be used to handle the request. + * @see Session + */ + @Nullable // because ResultT could be Void + ResultT execute( + @NonNull RequestT request, @NonNull GenericType resultType); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java b/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java new file mode 100644 index 00000000000..66e3bac2df7 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/session/SessionBuilder.java @@ -0,0 +1,408 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.session; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.internal.core.ContactPoints; +import com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoader; +import com.datastax.oss.driver.internal.core.context.DefaultDriverContext; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.function.Predicate; +import java.util.function.Supplier; +import net.jcip.annotations.NotThreadSafe; + +/** + * Base implementation to build session instances. + * + *

You only need to deal with this directly if you use custom driver extensions. For the default + * session implementation, see {@link CqlSession#builder()}. + */ +@NotThreadSafe +public abstract class SessionBuilder { + + @SuppressWarnings("unchecked") + protected final SelfT self = (SelfT) this; + + protected DriverConfigLoader configLoader; + protected Set programmaticContactPoints = new HashSet<>(); + protected List> typeCodecs = new ArrayList<>(); + private NodeStateListener nodeStateListener; + private SchemaChangeListener schemaChangeListener; + protected RequestTracker requestTracker; + private ImmutableMap.Builder localDatacenters = ImmutableMap.builder(); + private ImmutableMap.Builder> nodeFilters = ImmutableMap.builder(); + protected CqlIdentifier keyspace; + private ClassLoader classLoader = null; + + /** + * Sets the configuration loader to use. + * + *

If you don't call this method, the builder will use the default implementation, based on the + * Typesafe config library. More precisely: + * + *

    + *
  • configuration properties are loaded and merged from the following (first-listed are + * higher priority): + *
      + *
    • system properties + *
    • {@code application.conf} (all resources on classpath with this name) + *
    • {@code application.json} (all resources on classpath with this name) + *
    • {@code application.properties} (all resources on classpath with this name) + *
    • {@code reference.conf} (all resources on classpath with this name) + *
    + *
  • the resulting configuration is expected to contain a {@code datastax-java-driver} + * section. + *
  • that section is validated against the {@link DefaultDriverOption core driver options}. + *
+ * + * The core driver JAR includes a {@code reference.conf} file with sensible defaults for all + * mandatory options, except the contact points. + * + * @see Typesafe config's + * standard loading behavior + */ + @NonNull + public SelfT withConfigLoader(@Nullable DriverConfigLoader configLoader) { + this.configLoader = configLoader; + return self; + } + + @NonNull + protected DriverConfigLoader defaultConfigLoader() { + return new DefaultDriverConfigLoader(); + } + + /** + * Adds contact points to use for the initial connection to the cluster. + * + *

These are addresses of Cassandra nodes that the driver uses to discover the cluster + * topology. Only one contact point is required (the driver will retrieve the address of the other + * nodes automatically), but it is usually a good idea to provide more than one contact point, + * because if that single contact point is unavailable, the driver cannot initialize itself + * correctly. + * + *

Contact points can also be provided statically in the configuration. If both are specified, + * they will be merged. If both are absent, the driver will default to 127.0.0.1:9042. + * + *

Contrary to the configuration, DNS names with multiple A-records will not be handled here. + * If you need that, extract them manually with {@link java.net.InetAddress#getAllByName(String)} + * before calling this method. Similarly, if you need connect addresses to stay unresolved, make + * sure you pass unresolved instances here (see {@code advanced.resolve-contact-points} in the + * configuration for more explanations). + */ + @NonNull + public SelfT addContactPoints(@NonNull Collection contactPoints) { + for (InetSocketAddress contactPoint : contactPoints) { + addContactPoint(contactPoint); + } + return self; + } + + /** + * Adds a contact point to use for the initial connection to the cluster. + * + * @see #addContactPoints(Collection) + */ + @NonNull + public SelfT addContactPoint(@NonNull InetSocketAddress contactPoint) { + this.programmaticContactPoints.add(new DefaultEndPoint(contactPoint)); + return self; + } + + /** + * Adds contact points to use for the initial connection to the cluster. + * + *

You only need this method if you use a custom {@link EndPoint} implementation. Otherwise, + * use {@link #addContactPoints(Collection)}. + */ + @NonNull + public SelfT addContactEndPoints(@NonNull Collection contactPoints) { + for (EndPoint contactPoint : contactPoints) { + addContactEndPoint(contactPoint); + } + return self; + } + + /** + * Adds a contact point to use for the initial connection to the cluster. + * + *

You only need this method if you use a custom {@link EndPoint} implementation. Otherwise, + * use {@link #addContactPoint(InetSocketAddress)}. + */ + @NonNull + public SelfT addContactEndPoint(@NonNull EndPoint contactPoint) { + this.programmaticContactPoints.add(contactPoint); + return self; + } + + /** + * Registers additional codecs for custom type mappings. + * + * @param typeCodecs neither the individual codecs, nor the vararg array itself, can be {@code + * null}. + */ + @NonNull + public SelfT addTypeCodecs(@NonNull TypeCodec... typeCodecs) { + Collections.addAll(this.typeCodecs, typeCodecs); + return self; + } + + /** + * Registers a node state listener to use with the session. + * + *

If the listener is specified programmatically with this method, it overrides the + * configuration (that is, the {@code metadata.node-state-listener.class} option will be ignored). + */ + @NonNull + public SelfT withNodeStateListener(@Nullable NodeStateListener nodeStateListener) { + this.nodeStateListener = nodeStateListener; + return self; + } + + /** + * Registers a schema change listener to use with the session. + * + *

If the listener is specified programmatically with this method, it overrides the + * configuration (that is, the {@code metadata.schema-change-listener.class} option will be + * ignored). + */ + @NonNull + public SelfT withSchemaChangeListener(@Nullable SchemaChangeListener schemaChangeListener) { + this.schemaChangeListener = schemaChangeListener; + return self; + } + + /** + * Register a request tracker to use with the session. + * + *

If the tracker is specified programmatically with this method, it overrides the + * configuration (that is, the {@code request.tracker.class} option will be ignored). + */ + @NonNull + public SelfT withRequestTracker(@Nullable RequestTracker requestTracker) { + this.requestTracker = requestTracker; + return self; + } + + /** + * Specifies the datacenter that is considered "local" by the load balancing policy. + * + *

This is a programmatic alternative to the configuration option {@code + * basic.load-balancing-policy.local-datacenter}. If this method is used, it takes precedence and + * overrides the configuration. + * + *

Note that this setting may or may not be relevant depending on the load balancing policy + * implementation in use. The driver's built-in {@code DefaultLoadBalancingPolicy} relies on it; + * if you use a third-party implementation, refer to their documentation. + */ + public SelfT withLocalDatacenter(@NonNull String profileName, @NonNull String localDatacenter) { + this.localDatacenters.put(profileName, localDatacenter); + return self; + } + + /** Alias to {@link #withLocalDatacenter(String, String)} for the default profile. */ + public SelfT withLocalDatacenter(@NonNull String localDatacenter) { + return withLocalDatacenter(DriverExecutionProfile.DEFAULT_NAME, localDatacenter); + } + + /** + * Adds a custom filter to include/exclude nodes for a particular execution profile. This assumes + * that you're also using a dedicated load balancing policy for that profile. + * + *

The predicate's {@link Predicate#test(Object) test()} method will be invoked each time the + * {@link LoadBalancingPolicy} processes a topology or state change: if it returns false, the + * policy will suggest distance IGNORED (meaning the driver won't ever connect to it if all + * policies agree), and never included in any query plan. + * + *

Note that this behavior is implemented in the default load balancing policy. If you use a + * custom policy implementation, you'll need to explicitly invoke the filter. + * + *

If the filter is specified programmatically with this method, it overrides the configuration + * (that is, the {@code load-balancing-policy.filter.class} option will be ignored). + * + * @see #withNodeFilter(Predicate) + */ + @NonNull + public SelfT withNodeFilter(@NonNull String profileName, @NonNull Predicate nodeFilter) { + this.nodeFilters.put(profileName, nodeFilter); + return self; + } + + /** Alias to {@link #withNodeFilter(String, Predicate)} for the default profile. */ + @NonNull + public SelfT withNodeFilter(@NonNull Predicate nodeFilter) { + return withNodeFilter(DriverExecutionProfile.DEFAULT_NAME, nodeFilter); + } + + /** + * Sets the keyspace to connect the session to. + * + *

Note that this can also be provided by the configuration; if both are defined, this method + * takes precedence. + */ + @NonNull + public SelfT withKeyspace(@Nullable CqlIdentifier keyspace) { + this.keyspace = keyspace; + return self; + } + + /** + * Shortcut for {@link #withKeyspace(CqlIdentifier) + * setKeyspace(CqlIdentifier.fromCql(keyspaceName))} + */ + @NonNull + public SelfT withKeyspace(@Nullable String keyspaceName) { + return withKeyspace(keyspaceName == null ? null : CqlIdentifier.fromCql(keyspaceName)); + } + + /** + * The {@link ClassLoader} to use to reflectively load class names defined in configuration. + * + *

This is typically only needed when using OSGi or other in environments where there are + * complex class loading requirements. + * + *

If null, the driver attempts to use {@link Thread#getContextClassLoader()} of the current + * thread or the same {@link ClassLoader} that loaded the core driver classes. + */ + @NonNull + public SelfT withClassLoader(@Nullable ClassLoader classLoader) { + this.classLoader = classLoader; + return self; + } + + /** + * Creates the session with the options set by this builder. + * + * @return a completion stage that completes with the session when it is fully initialized. + */ + @NonNull + public CompletionStage buildAsync() { + CompletionStage buildStage = buildDefaultSessionAsync(); + CompletionStage wrapStage = buildStage.thenApply(this::wrap); + // thenApply does not propagate cancellation (!) + CompletableFutures.propagateCancellation(wrapStage, buildStage); + return wrapStage; + } + + /** + * Convenience method to call {@link #buildAsync()} and block on the result. + * + *

This must not be called on a driver thread. + */ + @NonNull + public SessionT build() { + BlockingOperation.checkNotDriverThread(); + return CompletableFutures.getUninterruptibly(buildAsync()); + } + + protected abstract SessionT wrap(@NonNull CqlSession defaultSession); + + @NonNull + protected final CompletionStage buildDefaultSessionAsync() { + try { + DriverConfigLoader configLoader = buildIfNull(this.configLoader, this::defaultConfigLoader); + + DriverExecutionProfile defaultConfig = configLoader.getInitialConfig().getDefaultProfile(); + List configContactPoints = + defaultConfig.getStringList(DefaultDriverOption.CONTACT_POINTS, Collections.emptyList()); + boolean resolveAddresses = + defaultConfig.getBoolean(DefaultDriverOption.RESOLVE_CONTACT_POINTS, true); + + Set contactPoints = + ContactPoints.merge(programmaticContactPoints, configContactPoints, resolveAddresses); + + if (keyspace == null && defaultConfig.isDefined(DefaultDriverOption.SESSION_KEYSPACE)) { + keyspace = + CqlIdentifier.fromCql(defaultConfig.getString(DefaultDriverOption.SESSION_KEYSPACE)); + } + + return DefaultSession.init( + (InternalDriverContext) + buildContext( + configLoader, + typeCodecs, + nodeStateListener, + schemaChangeListener, + requestTracker, + localDatacenters.build(), + nodeFilters.build(), + classLoader), + contactPoints, + keyspace); + + } catch (Throwable t) { + // We construct the session synchronously (until the init() call), but async clients expect a + // failed future if anything goes wrong. So wrap any error from that synchronous part. + return CompletableFutures.failedFuture(t); + } + } + + /** + * This must return an instance of {@code InternalDriverContext} (it's not expressed + * directly in the signature to avoid leaking that type through the protected API). + */ + protected DriverContext buildContext( + DriverConfigLoader configLoader, + List> typeCodecs, + NodeStateListener nodeStateListener, + SchemaChangeListener schemaChangeListener, + RequestTracker requestTracker, + Map localDatacenters, + Map> nodeFilters, + ClassLoader classLoader) { + return new DefaultDriverContext( + configLoader, + typeCodecs, + nodeStateListener, + schemaChangeListener, + requestTracker, + localDatacenters, + nodeFilters, + classLoader); + } + + private static T buildIfNull(T value, Supplier builder) { + return (value == null) ? builder.get() : value; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/session/throttling/RequestThrottler.java b/core/src/main/java/com/datastax/oss/driver/api/core/session/throttling/RequestThrottler.java new file mode 100644 index 00000000000..21ae3b5e396 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/session/throttling/RequestThrottler.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.session.throttling; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.Closeable; + +/** Limits the number of concurrent requests executed by the driver. */ +public interface RequestThrottler extends Closeable { + + /** + * Registers a new request to be throttled. The throttler will invoke {@link + * Throttled#onThrottleReady(boolean)} when the request is allowed to proceed. + */ + void register(@NonNull Throttled request); + + /** + * Signals that a request has succeeded. This indicates to the throttler that another request + * might be started. + */ + void signalSuccess(@NonNull Throttled request); + + /** + * Signals that a request has failed. This indicates to the throttler that another request might + * be started. + */ + void signalError(@NonNull Throttled request, @NonNull Throwable error); + + /** + * Signals that a request has timed out. This indicates to the throttler that this request has + * stopped (if it was running already), or that it doesn't need to be started in the future. + * + *

Note: requests are responsible for handling their own timeout. The throttler does not + * perform time-based eviction on pending requests. + */ + void signalTimeout(@NonNull Throttled request); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/session/throttling/Throttled.java b/core/src/main/java/com/datastax/oss/driver/api/core/session/throttling/Throttled.java new file mode 100644 index 00000000000..f64ec481743 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/session/throttling/Throttled.java @@ -0,0 +1,44 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.session.throttling; + +import com.datastax.oss.driver.api.core.RequestThrottlingException; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A request that may be subjected to throttling by a {@link + * com.datastax.oss.driver.api.core.session.throttling.RequestThrottler}. + */ +public interface Throttled { + + /** + * Invoked by the throttler to indicate that the request can now start. The request must wait for + * this call until it does any "actual" work (typically, writing to a connection). + * + * @param wasDelayed indicates whether the throttler delayed at all; this is so that requests + * don't have to rely on measuring time to determine it (this is useful for metrics). + */ + void onThrottleReady(boolean wasDelayed); + + /** + * Invoked by the throttler to indicate that the request cannot be fulfilled. Typically, this + * means we've reached maximum capacity, and the request can't even be enqueued. This error must + * be rethrown to the client. + * + * @param error the error that the request should be completed (exceptionally) with. + */ + void onThrottleFailure(@NonNull RequestThrottlingException error); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/specex/SpeculativeExecutionPolicy.java b/core/src/main/java/com/datastax/oss/driver/api/core/specex/SpeculativeExecutionPolicy.java new file mode 100644 index 00000000000..149a2f3c285 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/specex/SpeculativeExecutionPolicy.java @@ -0,0 +1,55 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.specex; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * The policy that decides if the driver will send speculative queries to the next nodes when the + * current node takes too long to respond. + */ +public interface SpeculativeExecutionPolicy extends AutoCloseable { + + /** + * @param node the node that caused the speculative execution (that is, the node that was queried + * previously but was too slow to answer) + * @param keyspace the CQL keyspace currently associated to the session. This is set either + * through the configuration, by calling {@link SessionBuilder#withKeyspace(CqlIdentifier)}, + * or by manually executing a {@code USE} CQL statement. It can be {@code null} if the session + * has no keyspace. + * @param request the request to execute. + * @param runningExecutions the number of executions that are already running (including the + * initial, non-speculative request). For example, if this is 2 it means the initial attempt + * was sent, then the driver scheduled a first speculative execution, and it is now asking for + * the delay until the second speculative execution. + * @return the time (in milliseconds) until a speculative request is sent to the next node, or 0 + * to send it immediately, or a negative value to stop sending requests. + */ + long nextExecution( + @NonNull Node node, + @Nullable CqlIdentifier keyspace, + @NonNull Request request, + int runningExecutions); + + /** Called when the cluster that this policy is associated with closes. */ + @Override + void close(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/ssl/SslEngineFactory.java b/core/src/main/java/com/datastax/oss/driver/api/core/ssl/SslEngineFactory.java new file mode 100644 index 00000000000..a001c696fe0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/ssl/SslEngineFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.ssl; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import edu.umd.cs.findbugs.annotations.NonNull; +import javax.net.ssl.SSLEngine; + +/** + * Extension point to configure SSL based on the built-in JDK implementation. + * + *

Note that, for advanced use cases (such as bypassing the JDK in favor of another SSL + * implementation), the driver's internal API provides a lower-level interface: {@link + * com.datastax.oss.driver.internal.core.ssl.SslHandlerFactory}. + */ +public interface SslEngineFactory extends AutoCloseable { + /** + * Creates a new SSL engine each time a connection is established. + * + * @param remoteEndpoint the remote endpoint we are connecting to (the address of the Cassandra + * node). + */ + @NonNull + SSLEngine newSslEngine(@NonNull EndPoint remoteEndpoint); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/ssl/package-info.java b/core/src/main/java/com/datastax/oss/driver/api/core/ssl/package-info.java new file mode 100644 index 00000000000..343a8ab0360 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/ssl/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** Support for secured communication between the driver and Cassandra nodes. */ +package com.datastax.oss.driver.api.core.ssl; diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/time/TimestampGenerator.java b/core/src/main/java/com/datastax/oss/driver/api/core/time/TimestampGenerator.java new file mode 100644 index 00000000000..cc2fd76016f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/time/TimestampGenerator.java @@ -0,0 +1,38 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.time; + +/** + * Generates client-side, microsecond-precision query timestamps. + * + *

These timestamps are used to order queries server-side, and resolve potential conflicts. + */ +public interface TimestampGenerator extends AutoCloseable { + + /** + * Returns the next timestamp, in microseconds. + * + *

The timestamps returned by this method should be monotonic; that is, successive invocations + * should return strictly increasing results. Note that this might not be possible using the clock + * alone, if it is not precise enough; alternative strategies might include incrementing the last + * returned value if the clock tick hasn't changed, and possibly drifting in the future. See the + * built-in driver implementations for more details. + * + * @return the next timestamp, or {@link Long#MIN_VALUE} to indicate that the driver should not + * send one with the query (and let Cassandra generate a server-side timestamp). + */ + long next(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/tracker/RequestTracker.java b/core/src/main/java/com/datastax/oss/driver/api/core/tracker/RequestTracker.java new file mode 100644 index 00000000000..abab882544b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/tracker/RequestTracker.java @@ -0,0 +1,95 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.tracker; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** + * Tracks request execution for a session. + * + *

There is exactly one tracker per {@link Session}. It can be provided either via the + * configuration (see {@code reference.conf} in the manual or core driver JAR), or programmatically + * via {@link SessionBuilder#withRequestTracker(RequestTracker)}. + */ +public interface RequestTracker extends AutoCloseable { + + /** + * Invoked each time a request succeeds. + * + * @param latencyNanos the overall execution time (from the {@link Session#execute(Request, + * GenericType) session.execute} call until the result is made available to the client). + * @param executionProfile the execution profile of this request. + * @param node the node that returned the successful response. + */ + default void onSuccess( + @NonNull Request request, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node) {} + + /** + * Invoked each time a request fails. + * + * @param latencyNanos the overall execution time (from the {@link Session#execute(Request, + * GenericType) session.execute} call until the error is propagated to the client). + * @param executionProfile the execution profile of this request. + * @param node the node that returned the error response, or {@code null} if the error occurred + */ + default void onError( + @NonNull Request request, + @NonNull Throwable error, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + @Nullable Node node) {} + + /** + * Invoked each time a request fails at the node level. Similar to {@link #onError(Request, + * Throwable, long, DriverExecutionProfile, Node)} but at a per node level. + * + * @param latencyNanos the overall execution time (from the {@link Session#execute(Request, + * GenericType) session.execute} call until the error is propagated to the client). + * @param executionProfile the execution profile of this request. + * @param node the node that returned the error response. + */ + default void onNodeError( + @NonNull Request request, + @NonNull Throwable error, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node) {} + + /** + * Invoked each time a request succeeds at the node level. Similar to {@link #onSuccess(Request, + * long, DriverExecutionProfile, Node)} but at per Node level. + * + * @param latencyNanos the overall execution time (from the {@link Session#execute(Request, + * GenericType) session.execute} call until the result is made available to the client). + * @param executionProfile the execution profile of this request. + * @param node the node that returned the successful response. + */ + default void onNodeSuccess( + @NonNull Request request, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node) {} +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/CustomType.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/CustomType.java new file mode 100644 index 00000000000..bc4f5ce7c5a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/CustomType.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type; + +import com.datastax.oss.protocol.internal.ProtocolConstants; +import edu.umd.cs.findbugs.annotations.NonNull; + +public interface CustomType extends DataType { + /** + * The fully qualified name of the subtype of {@code org.apache.cassandra.db.marshal.AbstractType} + * that represents this type server-side. + */ + @NonNull + String getClassName(); + + @NonNull + @Override + default String asCql(boolean includeFrozen, boolean pretty) { + return String.format("'%s'", getClassName()); + } + + @Override + default int getProtocolCode() { + return ProtocolConstants.DataType.CUSTOM; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/DataType.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/DataType.java new file mode 100644 index 00000000000..87dc34144f7 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/DataType.java @@ -0,0 +1,47 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.detach.Detachable; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The type of a CQL column, field or function argument. + * + *

The default implementations returned by the driver are immutable and serializable. If you + * write your own implementations, they should at least be thread-safe; serializability is not + * mandatory, but recommended for use with some 3rd-party tools like Apache Spark ™. + * + * @see DataTypes + */ +public interface DataType extends Detachable { + /** The code of the data type in the native protocol specification. */ + int getProtocolCode(); + + /** + * Builds an appropriate representation for use in a CQL query. + * + * @param includeFrozen whether to include the {@code frozen<...>} keyword if applicable. This + * will need to be set depending on where the result is used: for example, {@code CREATE + * TABLE} statements use the frozen keyword, whereas it should never appear in {@code CREATE + * FUNCTION}. + * @param pretty whether to pretty-print UDT names (as described in {@link + * CqlIdentifier#asCql(boolean)}. + */ + @NonNull + String asCql(boolean includeFrozen, boolean pretty); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/DataTypes.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/DataTypes.java new file mode 100644 index 00000000000..0a61314ca71 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/DataTypes.java @@ -0,0 +1,121 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type; + +import com.datastax.oss.driver.api.core.detach.Detachable; +import com.datastax.oss.driver.internal.core.type.DefaultCustomType; +import com.datastax.oss.driver.internal.core.type.DefaultListType; +import com.datastax.oss.driver.internal.core.type.DefaultMapType; +import com.datastax.oss.driver.internal.core.type.DefaultSetType; +import com.datastax.oss.driver.internal.core.type.DefaultTupleType; +import com.datastax.oss.driver.internal.core.type.PrimitiveType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Arrays; + +/** Constants and factory methods to obtain data type instances. */ +public class DataTypes { + + public static final DataType ASCII = new PrimitiveType(ProtocolConstants.DataType.ASCII); + public static final DataType BIGINT = new PrimitiveType(ProtocolConstants.DataType.BIGINT); + public static final DataType BLOB = new PrimitiveType(ProtocolConstants.DataType.BLOB); + public static final DataType BOOLEAN = new PrimitiveType(ProtocolConstants.DataType.BOOLEAN); + public static final DataType COUNTER = new PrimitiveType(ProtocolConstants.DataType.COUNTER); + public static final DataType DECIMAL = new PrimitiveType(ProtocolConstants.DataType.DECIMAL); + public static final DataType DOUBLE = new PrimitiveType(ProtocolConstants.DataType.DOUBLE); + public static final DataType FLOAT = new PrimitiveType(ProtocolConstants.DataType.FLOAT); + public static final DataType INT = new PrimitiveType(ProtocolConstants.DataType.INT); + public static final DataType TIMESTAMP = new PrimitiveType(ProtocolConstants.DataType.TIMESTAMP); + public static final DataType UUID = new PrimitiveType(ProtocolConstants.DataType.UUID); + public static final DataType VARINT = new PrimitiveType(ProtocolConstants.DataType.VARINT); + public static final DataType TIMEUUID = new PrimitiveType(ProtocolConstants.DataType.TIMEUUID); + public static final DataType INET = new PrimitiveType(ProtocolConstants.DataType.INET); + public static final DataType DATE = new PrimitiveType(ProtocolConstants.DataType.DATE); + public static final DataType TEXT = new PrimitiveType(ProtocolConstants.DataType.VARCHAR); + public static final DataType TIME = new PrimitiveType(ProtocolConstants.DataType.TIME); + public static final DataType SMALLINT = new PrimitiveType(ProtocolConstants.DataType.SMALLINT); + public static final DataType TINYINT = new PrimitiveType(ProtocolConstants.DataType.TINYINT); + public static final DataType DURATION = new PrimitiveType(ProtocolConstants.DataType.DURATION); + + @NonNull + public static DataType custom(@NonNull String className) { + // In protocol v4, duration is implemented as a custom type + if ("org.apache.cassandra.db.marshal.DurationType".equals(className)) { + return DURATION; + } else { + return new DefaultCustomType(className); + } + } + + @NonNull + public static ListType listOf(@NonNull DataType elementType) { + return new DefaultListType(elementType, false); + } + + @NonNull + public static ListType listOf(@NonNull DataType elementType, boolean frozen) { + return new DefaultListType(elementType, frozen); + } + + @NonNull + public static ListType frozenListOf(@NonNull DataType elementType) { + return new DefaultListType(elementType, true); + } + + @NonNull + public static SetType setOf(@NonNull DataType elementType) { + return new DefaultSetType(elementType, false); + } + + @NonNull + public static SetType setOf(@NonNull DataType elementType, boolean frozen) { + return new DefaultSetType(elementType, frozen); + } + + @NonNull + public static SetType frozenSetOf(@NonNull DataType elementType) { + return new DefaultSetType(elementType, true); + } + + @NonNull + public static MapType mapOf(@NonNull DataType keyType, @NonNull DataType valueType) { + return new DefaultMapType(keyType, valueType, false); + } + + @NonNull + public static MapType mapOf( + @NonNull DataType keyType, @NonNull DataType valueType, boolean frozen) { + return new DefaultMapType(keyType, valueType, frozen); + } + + @NonNull + public static MapType frozenMapOf(@NonNull DataType keyType, @NonNull DataType valueType) { + return new DefaultMapType(keyType, valueType, true); + } + + /** + * Builds a new, detached tuple type. + * + * @param componentTypes neither the individual types, nor the vararg array itself, can be {@code + * null}. + * @see Detachable + */ + @NonNull + public static TupleType tupleOf(@NonNull DataType... componentTypes) { + return new DefaultTupleType(ImmutableList.copyOf(Arrays.asList(componentTypes))); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/ListType.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/ListType.java new file mode 100644 index 00000000000..1bafb1693d7 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/ListType.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type; + +import com.datastax.oss.protocol.internal.ProtocolConstants; +import edu.umd.cs.findbugs.annotations.NonNull; + +public interface ListType extends DataType { + + @NonNull + DataType getElementType(); + + boolean isFrozen(); + + @NonNull + @Override + default String asCql(boolean includeFrozen, boolean pretty) { + String template = (isFrozen() && includeFrozen) ? "frozen>" : "list<%s>"; + return String.format(template, getElementType().asCql(includeFrozen, pretty)); + } + + @Override + default int getProtocolCode() { + return ProtocolConstants.DataType.LIST; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/MapType.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/MapType.java new file mode 100644 index 00000000000..e79990f0782 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/MapType.java @@ -0,0 +1,45 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type; + +import com.datastax.oss.protocol.internal.ProtocolConstants; +import edu.umd.cs.findbugs.annotations.NonNull; + +public interface MapType extends DataType { + + @NonNull + DataType getKeyType(); + + @NonNull + DataType getValueType(); + + boolean isFrozen(); + + @NonNull + @Override + default String asCql(boolean includeFrozen, boolean pretty) { + String template = (isFrozen() && includeFrozen) ? "frozen>" : "map<%s, %s>"; + return String.format( + template, + getKeyType().asCql(includeFrozen, pretty), + getValueType().asCql(includeFrozen, pretty)); + } + + @Override + default int getProtocolCode() { + return ProtocolConstants.DataType.MAP; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/SetType.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/SetType.java new file mode 100644 index 00000000000..eadd0a702e3 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/SetType.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type; + +import com.datastax.oss.protocol.internal.ProtocolConstants; +import edu.umd.cs.findbugs.annotations.NonNull; + +public interface SetType extends DataType { + + @NonNull + DataType getElementType(); + + boolean isFrozen(); + + @NonNull + @Override + default String asCql(boolean includeFrozen, boolean pretty) { + String template = (isFrozen() && includeFrozen) ? "frozen>" : "set<%s>"; + return String.format(template, getElementType().asCql(includeFrozen, pretty)); + } + + @Override + default int getProtocolCode() { + return ProtocolConstants.DataType.SET; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/TupleType.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/TupleType.java new file mode 100644 index 00000000000..a22bca8856d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/TupleType.java @@ -0,0 +1,77 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type; + +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +public interface TupleType extends DataType { + + @NonNull + List getComponentTypes(); + + @NonNull + TupleValue newValue(); + + /** + * Creates a new instance with the specified values for the fields. + * + *

To encode the values, this method uses the {@link CodecRegistry} that this type is {@link + * #getAttachmentPoint() attached} to; it looks for the best codec to handle the target CQL type + * and actual runtime type of each value (see {@link CodecRegistry#codecFor(DataType, Object)}). + * + * @param values the values of the tuple's fields. They must be in the same order as the fields in + * the tuple's definition. You can specify less values than there are fields (the remaining + * ones will be set to NULL), but not more (a runtime exception will be thrown). Individual + * values can be {@code null}, but the array itself can't. + * @throws IllegalArgumentException if there are too many values. + */ + @NonNull + TupleValue newValue(@NonNull Object... values); + + @NonNull + AttachmentPoint getAttachmentPoint(); + + @NonNull + @Override + default String asCql(boolean includeFrozen, boolean pretty) { + StringBuilder builder = new StringBuilder(); + // Tuples are always frozen + if (includeFrozen) { + builder.append("frozen<"); + } + boolean first = true; + for (DataType type : getComponentTypes()) { + builder.append(first ? "tuple<" : ", "); + first = false; + builder.append(type.asCql(includeFrozen, pretty)); + } + builder.append('>'); + if (includeFrozen) { + builder.append('>'); + } + return builder.toString(); + } + + @Override + default int getProtocolCode() { + return ProtocolConstants.DataType.TUPLE; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/UserDefinedType.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/UserDefinedType.java new file mode 100644 index 00000000000..b032151cc0e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/UserDefinedType.java @@ -0,0 +1,133 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.metadata.schema.Describable; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.metadata.schema.ScriptBuilder; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.List; + +public interface UserDefinedType extends DataType, Describable { + + @Nullable // because of ShallowUserDefinedType usage in the query builder + CqlIdentifier getKeyspace(); + + @NonNull + CqlIdentifier getName(); + + boolean isFrozen(); + + @NonNull + List getFieldNames(); + + int firstIndexOf(CqlIdentifier id); + + int firstIndexOf(String name); + + default boolean contains(@NonNull CqlIdentifier id) { + return firstIndexOf(id) >= 0; + } + + default boolean contains(@NonNull String name) { + return firstIndexOf(name) >= 0; + } + + @NonNull + List getFieldTypes(); + + @NonNull + UserDefinedType copy(boolean newFrozen); + + @NonNull + UdtValue newValue(); + + /** + * Creates a new instance with the specified values for the fields. + * + *

To encode the values, this method uses the {@link CodecRegistry} that this type is {@link + * #getAttachmentPoint() attached} to; it looks for the best codec to handle the target CQL type + * and actual runtime type of each value (see {@link CodecRegistry#codecFor(DataType, Object)}). + * + * @param fields the value of the fields. They must be in the same order as the fields in the + * type's definition. You can specify less values than there are fields (the remaining ones + * will be set to NULL), but not more (a runtime exception will be thrown). Individual values + * can be {@code null}, but the array itself can't. + * @throws IllegalArgumentException if there are too many values. + */ + @NonNull + UdtValue newValue(@NonNull Object... fields); + + @NonNull + AttachmentPoint getAttachmentPoint(); + + @NonNull + @Override + default String asCql(boolean includeFrozen, boolean pretty) { + if (getKeyspace() != null) { + String template = (isFrozen() && includeFrozen) ? "frozen<%s.%s>" : "%s.%s"; + return String.format(template, getKeyspace().asCql(pretty), getName().asCql(pretty)); + } else { + String template = (isFrozen() && includeFrozen) ? "frozen<%s>" : "%s"; + return String.format(template, getName().asCql(pretty)); + } + } + + @NonNull + @Override + default String describe(boolean pretty) { + ScriptBuilder builder = new ScriptBuilder(pretty); + + builder + .append("CREATE TYPE ") + .append(getKeyspace()) + .append(".") + .append(getName()) + .append(" (") + .newLine() + .increaseIndent(); + + List fieldNames = getFieldNames(); + List fieldTypes = getFieldTypes(); + int fieldCount = fieldNames.size(); + for (int i = 0; i < fieldCount; i++) { + builder.append(fieldNames.get(i)).append(" ").append(fieldTypes.get(i).asCql(true, pretty)); + if (i < fieldCount - 1) { + builder.append(","); + } + builder.newLine(); + } + builder.decreaseIndent().append(");"); + return builder.build(); + } + + @NonNull + @Override + default String describeWithChildren(boolean pretty) { + // No children (if it uses other types, they're considered dependencies, not sub-elements) + return describe(pretty); + } + + @Override + default int getProtocolCode() { + return ProtocolConstants.DataType.UDT; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/CodecNotFoundException.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/CodecNotFoundException.java new file mode 100644 index 00000000000..4d46f253915 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/CodecNotFoundException.java @@ -0,0 +1,65 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec; + +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +/** Thrown when a suitable {@link TypeCodec} cannot be found by the {@link CodecRegistry}. */ +public class CodecNotFoundException extends RuntimeException { + + private final DataType cqlType; + + private final GenericType javaType; + + public CodecNotFoundException(@Nullable DataType cqlType, @Nullable GenericType javaType) { + this( + String.format("Codec not found for requested operation: [%s <-> %s]", cqlType, javaType), + null, + cqlType, + javaType); + } + + public CodecNotFoundException( + @NonNull Throwable cause, @Nullable DataType cqlType, @Nullable GenericType javaType) { + this( + String.format( + "Error while looking up codec for requested operation: [%s <-> %s]", cqlType, javaType), + cause, + cqlType, + javaType); + } + + private CodecNotFoundException( + String msg, Throwable cause, DataType cqlType, GenericType javaType) { + super(msg, cause); + this.cqlType = cqlType; + this.javaType = javaType; + } + + @Nullable + public DataType getCqlType() { + return cqlType; + } + + @Nullable + public GenericType getJavaType() { + return javaType; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveBooleanCodec.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveBooleanCodec.java new file mode 100644 index 00000000000..45f3577284a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveBooleanCodec.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; + +/** + * A specialized boolean codec that knows how to deal with primitive types. + * + *

If the codec registry returns an instance of this type, the driver's boolean getters will use + * it to avoid boxing. + */ +public interface PrimitiveBooleanCodec extends TypeCodec { + + @Nullable + ByteBuffer encodePrimitive(boolean value, @NonNull ProtocolVersion protocolVersion); + + boolean decodePrimitive(@Nullable ByteBuffer value, @NonNull ProtocolVersion protocolVersion); + + @Nullable + @Override + default ByteBuffer encode(@Nullable Boolean value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : encodePrimitive(value, protocolVersion); + } + + @Nullable + @Override + default Boolean decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null || bytes.remaining() == 0) + ? null + : decodePrimitive(bytes, protocolVersion); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveByteCodec.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveByteCodec.java new file mode 100644 index 00000000000..119b950cfb9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveByteCodec.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; + +/** + * A specialized byte codec that knows how to deal with primitive types. + * + *

If the codec registry returns an instance of this type, the driver's byte getters will use it + * to avoid boxing. + */ +public interface PrimitiveByteCodec extends TypeCodec { + + @Nullable + ByteBuffer encodePrimitive(byte value, @NonNull ProtocolVersion protocolVersion); + + byte decodePrimitive(@Nullable ByteBuffer value, @NonNull ProtocolVersion protocolVersion); + + @Nullable + @Override + default ByteBuffer encode(@Nullable Byte value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : encodePrimitive(value, protocolVersion); + } + + @Nullable + @Override + default Byte decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null || bytes.remaining() == 0) + ? null + : decodePrimitive(bytes, protocolVersion); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveDoubleCodec.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveDoubleCodec.java new file mode 100644 index 00000000000..87d504a1ee3 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveDoubleCodec.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; + +/** + * A specialized double codec that knows how to deal with primitive types. + * + *

If the codec registry returns an instance of this type, the driver's double getters will use + * it to avoid boxing. + */ +public interface PrimitiveDoubleCodec extends TypeCodec { + + @Nullable + ByteBuffer encodePrimitive(double value, @NonNull ProtocolVersion protocolVersion); + + double decodePrimitive(@Nullable ByteBuffer value, @NonNull ProtocolVersion protocolVersion); + + @Nullable + @Override + default ByteBuffer encode(@Nullable Double value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : encodePrimitive(value, protocolVersion); + } + + @Nullable + @Override + default Double decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null || bytes.remaining() == 0) + ? null + : decodePrimitive(bytes, protocolVersion); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveFloatCodec.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveFloatCodec.java new file mode 100644 index 00000000000..fcb048da7ae --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveFloatCodec.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; + +/** + * A specialized float codec that knows how to deal with primitive types. + * + *

If the codec registry returns an instance of this type, the driver's float getters will use it + * to avoid boxing. + */ +public interface PrimitiveFloatCodec extends TypeCodec { + + @Nullable + ByteBuffer encodePrimitive(float value, @NonNull ProtocolVersion protocolVersion); + + float decodePrimitive(@Nullable ByteBuffer value, @NonNull ProtocolVersion protocolVersion); + + @Nullable + @Override + default ByteBuffer encode(@Nullable Float value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : encodePrimitive(value, protocolVersion); + } + + @Nullable + @Override + default Float decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null || bytes.remaining() == 0) + ? null + : decodePrimitive(bytes, protocolVersion); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveIntCodec.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveIntCodec.java new file mode 100644 index 00000000000..e029302f060 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveIntCodec.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; + +/** + * A specialized integer codec that knows how to deal with primitive types. + * + *

If the codec registry returns an instance of this type, the driver's integer getters will use + * it to avoid boxing. + */ +public interface PrimitiveIntCodec extends TypeCodec { + + @Nullable + ByteBuffer encodePrimitive(int value, @NonNull ProtocolVersion protocolVersion); + + int decodePrimitive(@Nullable ByteBuffer value, @NonNull ProtocolVersion protocolVersion); + + @Nullable + @Override + default ByteBuffer encode(@Nullable Integer value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : encodePrimitive(value, protocolVersion); + } + + @Nullable + @Override + default Integer decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null || bytes.remaining() == 0) + ? null + : decodePrimitive(bytes, protocolVersion); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveLongCodec.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveLongCodec.java new file mode 100644 index 00000000000..56eaeb3e52c --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveLongCodec.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; + +/** + * A specialized long codec that knows how to deal with primitive types. + * + *

If the codec registry returns an instance of this type, the driver's long getters will use it + * to avoid boxing. + */ +public interface PrimitiveLongCodec extends TypeCodec { + + @Nullable + ByteBuffer encodePrimitive(long value, @NonNull ProtocolVersion protocolVersion); + + long decodePrimitive(@Nullable ByteBuffer value, @NonNull ProtocolVersion protocolVersion); + + @Nullable + @Override + default ByteBuffer encode(@Nullable Long value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : encodePrimitive(value, protocolVersion); + } + + @Nullable + @Override + default Long decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null || bytes.remaining() == 0) + ? null + : decodePrimitive(bytes, protocolVersion); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveShortCodec.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveShortCodec.java new file mode 100644 index 00000000000..dfeb7f0c72a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/PrimitiveShortCodec.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; + +/** + * A specialized short codec that knows how to deal with primitive types. + * + *

If the codec registry returns an instance of this type, the driver's short getters will use it + * to avoid boxing. + */ +public interface PrimitiveShortCodec extends TypeCodec { + + @Nullable + ByteBuffer encodePrimitive(short value, @NonNull ProtocolVersion protocolVersion); + + short decodePrimitive(@Nullable ByteBuffer value, @NonNull ProtocolVersion protocolVersion); + + @Nullable + @Override + default ByteBuffer encode(@Nullable Short value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : encodePrimitive(value, protocolVersion); + } + + @Nullable + @Override + default Short decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null || bytes.remaining() == 0) + ? null + : decodePrimitive(bytes, protocolVersion); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/TypeCodec.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/TypeCodec.java new file mode 100644 index 00000000000..777d35b25fb --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/TypeCodec.java @@ -0,0 +1,235 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.metadata.schema.AggregateMetadata; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; + +/** + * Manages the two-way conversion between a CQL type and a Java type. + * + *

Type codec implementations: + * + *

    + *
  1. must be thread-safe. + *
  2. must perform fast and never block. + *
  3. must support all native protocol versions; it is not possible to use different + * codecs for the same types but under different protocol versions. + *
  4. must comply with the native protocol specifications; failing to do so will result + * in unexpected results and could cause the driver to crash. + *
  5. should be stateless and immutable. + *
  6. should interpret {@code null} values and empty byte buffers (i.e. + * {@link ByteBuffer#remaining()} == 0) in a reasonable way; usually, {@code + * NULL} CQL values should map to {@code null} references, but exceptions exist; e.g. for + * varchar types, a {@code NULL} CQL value maps to a {@code null} reference, whereas an empty + * buffer maps to an empty String. For collection types, it is also admitted that {@code NULL} + * CQL values map to empty Java collections instead of {@code null} references. In any case, + * the codec's behavior with respect to {@code null} values and empty ByteBuffers should be + * clearly documented. + *
  7. for Java types that have a primitive equivalent, should implement the appropriate + * "primitive" codec interface, e.g. {@link PrimitiveBooleanCodec} for {@code boolean}. This + * allows the driver to avoid the overhead of boxing when using primitive accessors such as + * {@link Row#getBoolean(int)}. + *
  8. when decoding, must not consume {@link ByteBuffer} instances by performing + * relative read operations that modify their current position; codecs should instead prefer + * absolute read methods or, if necessary, {@link ByteBuffer#duplicate() duplicate} their byte + * buffers prior to reading them. + *
+ */ +public interface TypeCodec { + + @NonNull + GenericType getJavaType(); + + @NonNull + DataType getCqlType(); + + /** + * Whether this codec is capable of processing the given Java type. + * + *

The default implementation is invariant with respect to the passed argument + * (through the usage of {@link GenericType#equals(Object)}) and it's strongly recommended not + * to modify this behavior. This means that a codec will only ever accept the exact + * Java type that it has been created for. + * + *

If the argument represents a Java primitive type, its wrapper type is considered instead. + */ + default boolean accepts(@NonNull GenericType javaType) { + Preconditions.checkNotNull(javaType); + return getJavaType().equals(javaType.wrap()); + } + + /** + * Whether this codec is capable of processing the given Java class. + * + *

This implementation simply compares the given class (or its wrapper type if it is a + * primitive type) against this codec's runtime (raw) class; it is invariant with respect + * to the passed argument (through the usage of {@link Class#equals(Object)} and it's strongly + * recommended not to modify this behavior. This means that a codec will only ever return + * {@code true} for the exact runtime (raw) Java class that it has been created for. + * + *

Implementors are encouraged to override this method if there is a more efficient way. In + * particular, if the codec targets a final class, the check can be done with a simple {@code ==}. + */ + default boolean accepts(@NonNull Class javaClass) { + Preconditions.checkNotNull(javaClass); + if (javaClass.isPrimitive()) { + if (javaClass == Boolean.TYPE) { + javaClass = Boolean.class; + } else if (javaClass == Character.TYPE) { + javaClass = Character.class; + } else if (javaClass == Byte.TYPE) { + javaClass = Byte.class; + } else if (javaClass == Short.TYPE) { + javaClass = Short.class; + } else if (javaClass == Integer.TYPE) { + javaClass = Integer.class; + } else if (javaClass == Long.TYPE) { + javaClass = Long.class; + } else if (javaClass == Float.TYPE) { + javaClass = Float.class; + } else if (javaClass == Double.TYPE) { + javaClass = Double.class; + } + } + return getJavaType().getRawType().equals(javaClass); + } + + /** + * Whether this codec is capable of encoding the given Java object. + * + *

The object's Java type is inferred from its runtime (raw) type, contrary to {@link + * #accepts(GenericType)} which is capable of handling generic types. + * + *

Contrary to other {@code accept} methods, this method's default implementation is + * covariant with respect to the passed argument (through the usage of {@link + * Class#isAssignableFrom(Class)}) and it's strongly recommended not to modify this + * behavior. This means that, by default, a codec will accept any subtype of the + * Java type that it has been created for. This is so because codec lookups by arbitrary Java + * objects only make sense when attempting to encode, never when attempting to decode, and indeed + * the {@linkplain #encode(Object, ProtocolVersion) encode} method is covariant with {@code + * JavaTypeT}. + * + *

It can only handle non-parameterized types; codecs handling parameterized types, such as + * collection types, must override this method and perform some sort of "manual" inspection of the + * actual type parameters. + * + *

Similarly, codecs that only accept a partial subset of all possible values must override + * this method and manually inspect the object to check if it complies or not with the codec's + * limitations. + * + *

Finally, if the codec targets a non-generic Java class, it might be possible to implement + * this method with a simple {@code instanceof} check. + */ + default boolean accepts(@NonNull Object value) { + Preconditions.checkNotNull(value); + return getJavaType().getRawType().isAssignableFrom(value.getClass()); + } + + /** Whether this codec is capable of processing the given CQL type. */ + default boolean accepts(@NonNull DataType cqlType) { + Preconditions.checkNotNull(cqlType); + return this.getCqlType().equals(cqlType); + } + + /** + * Encodes the given value in the binary format of the CQL type handled by this codec. + * + *

    + *
  • Null values should be gracefully handled and no exception should be raised; they should + * be considered as the equivalent of a NULL CQL value; + *
  • Codecs for CQL collection types should not permit null elements; + *
  • Codecs for CQL collection types should treat a {@code null} input as the equivalent of an + * empty collection. + *
+ */ + @Nullable + ByteBuffer encode(@Nullable JavaTypeT value, @NonNull ProtocolVersion protocolVersion); + + /** + * Decodes a value from the binary format of the CQL type handled by this codec. + * + *
    + *
  • Null or empty buffers should be gracefully handled and no exception should be raised; + * they should be considered as the equivalent of a NULL CQL value and, in most cases, + * should map to {@code null} or a default value for the corresponding Java type, if + * applicable; + *
  • Codecs for CQL collection types should clearly document whether they return immutable + * collections or not (note that the driver's default collection codecs return + * mutable collections); + *
  • Codecs for CQL collection types should avoid returning {@code null}; they should return + * empty collections instead (the driver's default collection codecs all comply with this + * rule); + *
  • The provided {@link ByteBuffer} should never be consumed by read operations that modify + * its current position; if necessary, {@link ByteBuffer#duplicate()} duplicate} it before + * consuming. + *
+ */ + @Nullable + JavaTypeT decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion); + + /** + * Formats the given value as a valid CQL literal according to the CQL type handled by this codec. + * + *

Implementors should take care of quoting and escaping the resulting CQL literal where + * applicable. Null values should be accepted; in most cases, implementations should return the + * CQL keyword {@code "NULL"} for {@code null} inputs. + * + *

Implementing this method is not strictly mandatory. It is used: + * + *

    + *
  1. by the request logger, if parameter logging is enabled; + *
  2. to format the INITCOND in {@link AggregateMetadata#describe(boolean)}; + *
  3. in the {@code toString()} representation of some driver objects (such as {@link UdtValue} + * and {@link TupleValue}), which is only used in driver logs; + *
  4. for literal values in the query builder (see {@code QueryBuilder#literal(Object, + * CodecRegistry)} and {@code QueryBuilder#literal(Object, TypeCodec)}). + *
+ * + * If you choose not to implement this method, don't throw an exception but instead return a + * constant string (for example "XxxCodec.format not implemented"). + */ + @NonNull + String format(@Nullable JavaTypeT value); + + /** + * Parse the given CQL literal into an instance of the Java type handled by this codec. + * + *

Implementors should take care of unquoting and unescaping the given CQL string where + * applicable. Null values and empty strings should be accepted, as well as the string {@code + * "NULL"}; in most cases, implementations should interpret these inputs has equivalent to a + * {@code null} reference. + * + *

Implementing this method is not strictly mandatory: internally, the driver only uses it to + * parse the INITCOND when building the {@link AggregateMetadata metadata of an aggregate + * function} (and in most cases it will use a built-in codec, unless the INITCOND has a custom + * type). + * + *

If you choose not to implement this method, don't throw an exception but instead return + * {@code null}. + */ + @Nullable + JavaTypeT parse(@Nullable String value); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/TypeCodecs.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/TypeCodecs.java new file mode 100644 index 00000000000..ac421f2a046 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/TypeCodecs.java @@ -0,0 +1,171 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec; + +import com.datastax.oss.driver.api.core.data.CqlDuration; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.CustomType; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.type.codec.BigIntCodec; +import com.datastax.oss.driver.internal.core.type.codec.BlobCodec; +import com.datastax.oss.driver.internal.core.type.codec.BooleanCodec; +import com.datastax.oss.driver.internal.core.type.codec.CounterCodec; +import com.datastax.oss.driver.internal.core.type.codec.CqlDurationCodec; +import com.datastax.oss.driver.internal.core.type.codec.CustomCodec; +import com.datastax.oss.driver.internal.core.type.codec.DateCodec; +import com.datastax.oss.driver.internal.core.type.codec.DecimalCodec; +import com.datastax.oss.driver.internal.core.type.codec.DoubleCodec; +import com.datastax.oss.driver.internal.core.type.codec.FloatCodec; +import com.datastax.oss.driver.internal.core.type.codec.InetCodec; +import com.datastax.oss.driver.internal.core.type.codec.IntCodec; +import com.datastax.oss.driver.internal.core.type.codec.ListCodec; +import com.datastax.oss.driver.internal.core.type.codec.MapCodec; +import com.datastax.oss.driver.internal.core.type.codec.SetCodec; +import com.datastax.oss.driver.internal.core.type.codec.SmallIntCodec; +import com.datastax.oss.driver.internal.core.type.codec.StringCodec; +import com.datastax.oss.driver.internal.core.type.codec.TimeCodec; +import com.datastax.oss.driver.internal.core.type.codec.TimeUuidCodec; +import com.datastax.oss.driver.internal.core.type.codec.TimestampCodec; +import com.datastax.oss.driver.internal.core.type.codec.TinyIntCodec; +import com.datastax.oss.driver.internal.core.type.codec.TupleCodec; +import com.datastax.oss.driver.internal.core.type.codec.UdtCodec; +import com.datastax.oss.driver.internal.core.type.codec.UuidCodec; +import com.datastax.oss.driver.internal.core.type.codec.VarIntCodec; +import com.datastax.oss.driver.internal.core.type.codec.ZonedTimestampCodec; +import com.datastax.oss.driver.shaded.guava.common.base.Charsets; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** Constants and factory methods to obtain type codec instances. */ +public class TypeCodecs { + + public static final PrimitiveBooleanCodec BOOLEAN = new BooleanCodec(); + public static final PrimitiveByteCodec TINYINT = new TinyIntCodec(); + public static final PrimitiveDoubleCodec DOUBLE = new DoubleCodec(); + public static final PrimitiveLongCodec COUNTER = new CounterCodec(); + public static final PrimitiveFloatCodec FLOAT = new FloatCodec(); + public static final PrimitiveIntCodec INT = new IntCodec(); + public static final PrimitiveLongCodec BIGINT = new BigIntCodec(); + public static final PrimitiveShortCodec SMALLINT = new SmallIntCodec(); + public static final TypeCodec TIMESTAMP = new TimestampCodec(); + + /** + * A codec that handles Apache Cassandra(R)'s timestamp type and maps it to Java's {@link + * ZonedDateTime}, using the system's {@linkplain ZoneId#systemDefault() default time zone} as its + * source of time zone information. + * + *

Note that Apache Cassandra(R)'s timestamp type does not store any time zone; this codec is + * provided merely as a convenience for users that need to deal with zoned timestamps in their + * applications. + * + * @see #ZONED_TIMESTAMP_UTC + * @see #zonedTimestampAt(ZoneId) + */ + public static final TypeCodec ZONED_TIMESTAMP_SYSTEM = new ZonedTimestampCodec(); + + /** + * A codec that handles Apache Cassandra(R)'s timestamp type and maps it to Java's {@link + * ZonedDateTime}, using {@link ZoneOffset#UTC} as its source of time zone information. + * + *

Note that Apache Cassandra(R)'s timestamp type does not store any time zone; this codec is + * provided merely as a convenience for users that need to deal with zoned timestamps in their + * applications. + * + * @see #ZONED_TIMESTAMP_SYSTEM + * @see #zonedTimestampAt(ZoneId) + */ + public static final TypeCodec ZONED_TIMESTAMP_UTC = + new ZonedTimestampCodec(ZoneOffset.UTC); + + public static final TypeCodec DATE = new DateCodec(); + public static final TypeCodec TIME = new TimeCodec(); + public static final TypeCodec BLOB = new BlobCodec(); + public static final TypeCodec TEXT = new StringCodec(DataTypes.TEXT, Charsets.UTF_8); + public static final TypeCodec ASCII = new StringCodec(DataTypes.ASCII, Charsets.US_ASCII); + public static final TypeCodec VARINT = new VarIntCodec(); + public static final TypeCodec DECIMAL = new DecimalCodec(); + public static final TypeCodec UUID = new UuidCodec(); + public static final TypeCodec TIMEUUID = new TimeUuidCodec(); + public static final TypeCodec INET = new InetCodec(); + public static final TypeCodec DURATION = new CqlDurationCodec(); + + @NonNull + public static TypeCodec custom(@NonNull DataType cqlType) { + Preconditions.checkArgument(cqlType instanceof CustomType, "cqlType must be a custom type"); + return new CustomCodec((CustomType) cqlType); + } + + @NonNull + public static TypeCodec> listOf(@NonNull TypeCodec elementCodec) { + return new ListCodec<>(DataTypes.listOf(elementCodec.getCqlType()), elementCodec); + } + + @NonNull + public static TypeCodec> setOf(@NonNull TypeCodec elementCodec) { + return new SetCodec<>(DataTypes.setOf(elementCodec.getCqlType()), elementCodec); + } + + @NonNull + public static TypeCodec> mapOf( + @NonNull TypeCodec keyCodec, @NonNull TypeCodec valueCodec) { + return new MapCodec<>( + DataTypes.mapOf(keyCodec.getCqlType(), valueCodec.getCqlType()), keyCodec, valueCodec); + } + + @NonNull + public static TypeCodec tupleOf(@NonNull TupleType cqlType) { + return new TupleCodec(cqlType); + } + + @NonNull + public static TypeCodec udtOf(@NonNull UserDefinedType cqlType) { + return new UdtCodec(cqlType); + } + + /** + * Returns a codec that handles Apache Cassandra(R)'s timestamp type and maps it to Java's {@link + * ZonedDateTime}, using the supplied {@link ZoneId} as its source of time zone information. + * + *

Note that Apache Cassandra(R)'s timestamp type does not store any time zone; the codecs + * created by this method are provided merely as a convenience for users that need to deal with + * zoned timestamps in their applications. + * + * @see #ZONED_TIMESTAMP_SYSTEM + * @see #ZONED_TIMESTAMP_UTC + */ + @NonNull + public static TypeCodec zonedTimestampAt(@NonNull ZoneId timeZone) { + return new ZonedTimestampCodec(timeZone); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/registry/CodecRegistry.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/registry/CodecRegistry.java new file mode 100644 index 00000000000..93456008bb6 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/codec/registry/CodecRegistry.java @@ -0,0 +1,168 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.codec.registry; + +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.data.GettableByIndex; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; + +/** + * Provides codecs to convert CQL types to their Java equivalent, and vice-versa. + * + *

Implementations MUST provide a default mapping for all CQL types (primitive types, and + * all the collections, tuples or user-defined types that can recursively be built from them — + * see {@link DataTypes}). + * + *

They may also provide additional mappings to other Java types (for use with methods such as + * {@link Row#get(int, Class)}, {@link TupleValue#set(int, Object, Class)}, etc.) + */ +public interface CodecRegistry { + /** + * An immutable instance, that only handles built-in driver types (that is, primitive types, and + * collections, tuples, and user defined types thereof). + */ + CodecRegistry DEFAULT = new DefaultCodecRegistry("default"); + + /** + * Returns a codec to handle the conversion between the given types. + * + *

This is used internally by the driver, in cases where both types are known, for example + * {@link GettableByIndex#getString(int) row.getString(0)} (Java type inferred from the method, + * CQL type known from the row metadata). + * + *

The driver's default registry implementation is invariant with regard to the Java + * type: for example, if {@code B extends A} and an {@code A<=>int} codec is registered, {@code + * codecFor(DataTypes.INT, B.class)} will not find that codec. This is because this method + * is used internally both for encoding and decoding, and covariance wouldn't work when decoding. + * + * @throws CodecNotFoundException if there is no such codec. + */ + @NonNull + TypeCodec codecFor( + @NonNull DataType cqlType, @NonNull GenericType javaType); + + /** + * Shortcut for {@link #codecFor(DataType, GenericType) codecFor(cqlType, + * GenericType.of(javaType))}. + * + *

Implementations may decide to override this method for performance reasons, if they have a + * way to avoid the overhead of wrapping. + * + * @throws CodecNotFoundException if there is no such codec. + */ + @NonNull + default TypeCodec codecFor( + @NonNull DataType cqlType, @NonNull Class javaType) { + return codecFor(cqlType, GenericType.of(javaType)); + } + + /** + * Returns a codec to convert the given CQL type to the Java type deemed most appropriate to + * represent it. + * + *

This is used internally by the driver, in cases where the Java type is not explicitly + * provided, for example {@link GettableByIndex#getObject(int) row.getObject(0)} (CQL type known + * from the row metadata, Java type unspecified). + * + *

The definition of "most appropriate" is left to the appreciation of the registry + * implementor. + * + * @throws CodecNotFoundException if there is no such codec. + */ + @NonNull + TypeCodec codecFor(@NonNull DataType cqlType); + + /** + * Returns a codec to convert the given Java type to the CQL type deemed most appropriate to + * represent it. + * + *

The driver does not use this method. It is provided as a convenience for third-party usage, + * for example if you were to generate a schema based on a set of Java classes. + * + *

The driver's default registry implementation is invariant with regard to the Java + * type: for example, if {@code B extends A} and an {@code A<=>int} codec is registered, {@code + * codecFor(DataTypes.INT, B.class)} will not find that codec. This is because we don't + * know whether this method will be used for encoding, decoding, or both. + * + * @throws CodecNotFoundException if there is no such codec. + */ + @NonNull + TypeCodec codecFor(@NonNull GenericType javaType); + + /** + * Shortcut for {@link #codecFor(GenericType) codecFor(GenericType.of(javaType))}. + * + *

Implementations may decide to override this method for performance reasons, if they have a + * way to avoid the overhead of wrapping. + * + * @throws CodecNotFoundException if there is no such codec. + */ + @NonNull + default TypeCodec codecFor(@NonNull Class javaType) { + return codecFor(GenericType.of(javaType)); + } + + /** + * Returns a codec to convert the given Java object to the given CQL type. + * + *

This is used internally by the driver when you bulk-set values in a {@link + * PreparedStatement#bind(Object...) bound statement}, {@link UserDefinedType#newValue(Object...) + * UDT} or {@link TupleType#newValue(Object...) tuple}. + * + *

Unlike other methods, the driver's default registry implementation is covariant + * with regard to the Java type: for example, if {@code B extends A} and an {@code A<=>int} codec + * is registered, {@code codecFor(DataTypes.INT, someB)} will find that codec. This is + * because this method is always used in encoding scenarios; if a bound statement has a value with + * a runtime type of {@code ArrayList}, it should be possible to encode it with a codec + * that accepts a {@code List}. + * + * @throws CodecNotFoundException if there is no such codec. + */ + @NonNull + TypeCodec codecFor(@NonNull DataType cqlType, @NonNull JavaTypeT value); + + /** + * Returns a codec to convert the given Java object to the CQL type deemed most appropriate to + * represent it. + * + *

This is used internally by the driver, in cases where the CQL type is unknown, for example + * for {@linkplain SimpleStatement#setPositionalValues(List) simple statement variables} (simple + * statements don't have access to schema metadata). + * + *

Unlike other methods, the driver's default registry implementation is covariant + * with regard to the Java type: for example, if {@code B extends A} and an {@code A<=>int} codec + * is registered, {@code codecFor(someB)} will find that codec. This is because this method + * is always used in encoding scenarios; if a simple statement has a value with a runtime type of + * {@code ArrayList}, it should be possible to encode it with a codec that accepts a + * {@code List}. + * + * @throws CodecNotFoundException if there is no such codec. + */ + @NonNull + TypeCodec codecFor(@NonNull JavaTypeT value); +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/reflect/GenericType.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/reflect/GenericType.java new file mode 100644 index 00000000000..daa269862c3 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/reflect/GenericType.java @@ -0,0 +1,353 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.reflect; + +import com.datastax.oss.driver.api.core.data.CqlDuration; +import com.datastax.oss.driver.api.core.data.GettableByIndex; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.shaded.guava.common.primitives.Primitives; +import com.datastax.oss.driver.shaded.guava.common.reflect.TypeParameter; +import com.datastax.oss.driver.shaded.guava.common.reflect.TypeResolver; +import com.datastax.oss.driver.shaded.guava.common.reflect.TypeToken; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import net.jcip.annotations.Immutable; + +/** + * Runtime representation of a generic Java type. + * + *

This is used by type codecs to indicate which Java types they accept ({@link + * TypeCodec#accepts(GenericType)}), and by generic getters and setters (such as {@link + * GettableByIndex#get(int, GenericType)} in the driver's query API. + * + *

There are various ways to build instances of this class: + * + *

By using one of the static factory methods: + * + *

{@code
+ * GenericType> stringListType = GenericType.listOf(String.class);
+ * }
+ * + * By using an anonymous class: + * + *
{@code
+ * GenericType> fooBarType = new GenericType>(){};
+ * }
+ * + * In a generic method, by using {@link #where(GenericTypeParameter, GenericType)} to substitute + * free type variables with runtime types: + * + *
{@code
+ *  GenericType> optionalOf(GenericType elementType) {
+ *   return new GenericType>() {}
+ *     .where(new GenericTypeParameter() {}, elementType);
+ * }
+ * ...
+ * GenericType>> optionalStringListType = optionalOf(GenericType.listOf(String.class));
+ * }
+ * + *

You are encouraged to store and reuse these instances. + * + *

Note that this class is a thin wrapper around Guava's {@code TypeToken}. The only reason why + * {@code TypeToken} is not used directly is because Guava is not exposed in the driver's public API + * (it's used internally, but shaded). + */ +@Immutable +public class GenericType { + + public static final GenericType BOOLEAN = of(Boolean.class); + public static final GenericType BYTE = of(Byte.class); + public static final GenericType DOUBLE = of(Double.class); + public static final GenericType FLOAT = of(Float.class); + public static final GenericType INTEGER = of(Integer.class); + public static final GenericType LONG = of(Long.class); + public static final GenericType SHORT = of(Short.class); + public static final GenericType INSTANT = of(Instant.class); + public static final GenericType ZONED_DATE_TIME = of(ZonedDateTime.class); + public static final GenericType LOCAL_DATE = of(LocalDate.class); + public static final GenericType LOCAL_TIME = of(LocalTime.class); + public static final GenericType BYTE_BUFFER = of(ByteBuffer.class); + public static final GenericType STRING = of(String.class); + public static final GenericType BIG_INTEGER = of(BigInteger.class); + public static final GenericType BIG_DECIMAL = of(BigDecimal.class); + public static final GenericType UUID = of(UUID.class); + public static final GenericType INET_ADDRESS = of(InetAddress.class); + public static final GenericType CQL_DURATION = of(CqlDuration.class); + public static final GenericType TUPLE_VALUE = of(TupleValue.class); + public static final GenericType UDT_VALUE = of(UdtValue.class); + + @NonNull + public static GenericType of(@NonNull Class type) { + return new SimpleGenericType<>(type); + } + + @NonNull + public static GenericType of(@NonNull java.lang.reflect.Type type) { + return new GenericType<>(TypeToken.of(type)); + } + + @NonNull + public static GenericType> listOf(@NonNull Class elementType) { + TypeToken> token = + new TypeToken>() {}.where(new TypeParameter() {}, TypeToken.of(elementType)); + return new GenericType<>(token); + } + + @NonNull + public static GenericType> listOf(@NonNull GenericType elementType) { + TypeToken> token = + new TypeToken>() {}.where(new TypeParameter() {}, elementType.token); + return new GenericType<>(token); + } + + @NonNull + public static GenericType> setOf(@NonNull Class elementType) { + TypeToken> token = + new TypeToken>() {}.where(new TypeParameter() {}, TypeToken.of(elementType)); + return new GenericType<>(token); + } + + @NonNull + public static GenericType> setOf(@NonNull GenericType elementType) { + TypeToken> token = + new TypeToken>() {}.where(new TypeParameter() {}, elementType.token); + return new GenericType<>(token); + } + + @NonNull + public static GenericType> mapOf( + @NonNull Class keyType, @NonNull Class valueType) { + TypeToken> token = + new TypeToken>() {}.where(new TypeParameter() {}, TypeToken.of(keyType)) + .where(new TypeParameter() {}, TypeToken.of(valueType)); + return new GenericType<>(token); + } + + @NonNull + public static GenericType> mapOf( + @NonNull GenericType keyType, @NonNull GenericType valueType) { + TypeToken> token = + new TypeToken>() {}.where(new TypeParameter() {}, keyType.token) + .where(new TypeParameter() {}, valueType.token); + return new GenericType<>(token); + } + + private final TypeToken token; + + private GenericType(TypeToken token) { + this.token = token; + } + + protected GenericType() { + this.token = new TypeToken(getClass()) {}; + } + + /** + * Returns true if this type is a supertype of the given {@code type}. "Supertype" is defined + * according to the rules for type + * arguments introduced with Java generics. + */ + public final boolean isSupertypeOf(@NonNull GenericType type) { + return token.isSupertypeOf(type.token); + } + + /** + * Returns true if this type is a subtype of the given {@code type}. "Subtype" is defined + * according to the rules for type + * arguments introduced with Java generics. + */ + public final boolean isSubtypeOf(@NonNull GenericType type) { + return token.isSubtypeOf(type.token); + } + + /** + * Returns true if this type is known to be an array type, such as {@code int[]}, {@code T[]}, + * {@code []>} etc. + */ + public final boolean isArray() { + return token.isArray(); + } + + /** Returns true if this type is one of the nine primitive types (including {@code void}). */ + public final boolean isPrimitive() { + return token.isPrimitive(); + } + + /** + * Returns the corresponding wrapper type if this is a primitive type; otherwise returns {@code + * this} itself. Idempotent. + */ + @NonNull + public final GenericType wrap() { + if (isPrimitive()) { + return new GenericType<>(token.wrap()); + } + return this; + } + + /** + * Returns the corresponding primitive type if this is a wrapper type; otherwise returns {@code + * this} itself. Idempotent. + */ + @NonNull + public final GenericType unwrap() { + if (Primitives.allWrapperTypes().contains(token.getRawType())) { + return new GenericType<>(token.unwrap()); + } + return this; + } + + /** + * Substitutes a free type variable with an actual type. See {@link GenericType this class's + * javadoc} for an example. + */ + @NonNull + public final GenericType where( + @NonNull GenericTypeParameter freeVariable, @NonNull GenericType actualType) { + TypeResolver resolver = + new TypeResolver().where(freeVariable.getTypeVariable(), actualType.__getToken().getType()); + Type resolvedType = resolver.resolveType(this.token.getType()); + @SuppressWarnings("unchecked") + TypeToken resolvedToken = (TypeToken) TypeToken.of(resolvedType); + return new GenericType<>(resolvedToken); + } + + /** + * Substitutes a free type variable with an actual type. See {@link GenericType this class's + * javadoc} for an example. + */ + @NonNull + public final GenericType where( + @NonNull GenericTypeParameter freeVariable, @NonNull Class actualType) { + return where(freeVariable, GenericType.of(actualType)); + } + + /** + * Returns the array component type if this type represents an array ({@code int[]}, {@code T[]}, + * {@code []>} etc.), or else {@code null} is returned. + */ + @Nullable + @SuppressWarnings("unchecked") + public final GenericType getComponentType() { + TypeToken componentTypeToken = token.getComponentType(); + return (componentTypeToken == null) ? null : new GenericType(componentTypeToken); + } + + /** + * Returns the raw type of {@code T}. Formally speaking, if {@code T} is returned by {@link + * java.lang.reflect.Method#getGenericReturnType}, the raw type is what's returned by {@link + * java.lang.reflect.Method#getReturnType} of the same method object. Specifically: + * + *

    + *
  • If {@code T} is a {@code Class} itself, {@code T} itself is returned. + *
  • If {@code T} is a parameterized type, the raw type of the parameterized type is returned. + *
  • If {@code T} is an array type , the returned type is the corresponding array class. For + * example: {@code List[] => List[]}. + *
  • If {@code T} is a type variable or a wildcard type, the raw type of the first upper bound + * is returned. For example: {@code => Foo}. + *
+ */ + @NonNull + public Class getRawType() { + return token.getRawType(); + } + + /** + * Returns the generic form of {@code superclass}. For example, if this is {@code + * ArrayList}, {@code Iterable} is returned given the input {@code + * Iterable.class}. + */ + @SuppressWarnings("unchecked") + @NonNull + public final GenericType getSupertype(@NonNull Class superclass) { + return new GenericType(token.getSupertype(superclass)); + } + + /** + * Returns subtype of {@code this} with {@code subclass} as the raw class. For example, if this is + * {@code Iterable} and {@code subclass} is {@code List}, {@code List} is + * returned. + */ + @SuppressWarnings("unchecked") + @NonNull + public final GenericType getSubtype(@NonNull Class subclass) { + return new GenericType(token.getSubtype(subclass)); + } + + /** Returns the represented type. */ + @NonNull + public final Type getType() { + return token.getType(); + } + + /** + * This method is for internal use, DO NOT use it from client code. + * + *

It leaks a shaded type. This should be part of the internal API, but due to internal + * implementation details it has to be exposed here. + * + * @leaks-private-api + */ + @NonNull + public TypeToken __getToken() { + return token; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof GenericType) { + GenericType that = (GenericType) other; + return this.token.equals(that.token); + } else { + return false; + } + } + + @Override + public int hashCode() { + return token.hashCode(); + } + + @Override + public String toString() { + return token.toString(); + } + + private static class SimpleGenericType extends GenericType { + SimpleGenericType(Class type) { + super(TypeToken.of(type)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/type/reflect/GenericTypeParameter.java b/core/src/main/java/com/datastax/oss/driver/api/core/type/reflect/GenericTypeParameter.java new file mode 100644 index 00000000000..d62e8890600 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/type/reflect/GenericTypeParameter.java @@ -0,0 +1,46 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.reflect; + +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import net.jcip.annotations.Immutable; + +/** + * Captures a free type variable that can be used in {@link GenericType#where(GenericTypeParameter, + * GenericType)}. + */ +@Immutable +@SuppressWarnings("unused") // for T (unfortunately has to cover the whole class) +public class GenericTypeParameter { + private final TypeVariable typeVariable; + + protected GenericTypeParameter() { + Type superclass = getClass().getGenericSuperclass(); + Preconditions.checkArgument( + superclass instanceof ParameterizedType, "%s isn't parameterized", superclass); + this.typeVariable = + (TypeVariable) ((ParameterizedType) superclass).getActualTypeArguments()[0]; + } + + @NonNull + public TypeVariable getTypeVariable() { + return typeVariable; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/core/uuid/Uuids.java b/core/src/main/java/com/datastax/oss/driver/api/core/uuid/Uuids.java new file mode 100644 index 00000000000..7e82b4c685f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/core/uuid/Uuids.java @@ -0,0 +1,388 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.uuid; + +import com.datastax.oss.driver.internal.core.os.Native; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.base.Charsets; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.lang.management.ManagementFactory; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Calendar; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Properties; +import java.util.Random; +import java.util.Set; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility methods to help working with UUIDs, and more specifically, with time-based UUIDs (also + * known as Version 1 UUIDs). + * + *

The algorithm to generate time-based UUIDs roughly follows the description in RFC-4122, but + * with the following adaptations: + * + *

    + *
  1. Since Java does not provide direct access to the host's MAC address, that information is + * replaced with a digest of all IP addresses available on the host; + *
  2. The process ID (PID) isn't easily available to Java either, so it is determined by one of + * the following methods, in the order they are listed below: + *
      + *
    1. If the System property {@value PID_SYSTEM_PROPERTY} is set then the + * value to use as a PID will be read from that property; + *
    2. Otherwise, if a native call to {@code POSIX.getpid()} is possible, then the PID will + * be read from that call; + *
    3. Otherwise, an attempt will be made to read the PID from JMX's {@link + * ManagementFactory#getRuntimeMXBean() RuntimeMXBean}, since most JVMs tend to use the + * JVM's PID as part of that MXBean name (however that behavior is not officially part + * of the specification, so it may not work for all JVMs); + *
    4. If all of the above fail, a random integer will be generated and used as a surrogate + * PID. + *
    + *
+ * + * @see JAVA-444 + * @see A Universally Unique IDentifier (UUID) URN + * Namespace (RFC 4122) + */ +public final class Uuids { + + /** The system property to use to force the value of the process ID ({@value}). */ + public static final String PID_SYSTEM_PROPERTY = "com.datastax.oss.driver.PID"; + + private static final Logger LOG = LoggerFactory.getLogger(Uuids.class); + + private Uuids() {} + + private static final long START_EPOCH = makeEpoch(); + private static final long CLOCK_SEQ_AND_NODE = makeClockSeqAndNode(); + + // The min and max possible lsb for a UUID. + // + // This is not 0 and all 1's because Cassandra's TimeUUIDType compares the lsb parts as signed + // byte arrays. So the min value is 8 times -128 and the max is 8 times +127. + // + // We ignore the UUID variant (namely, MIN_CLOCK_SEQ_AND_NODE has variant 2 as it should, but + // MAX_CLOCK_SEQ_AND_NODE has variant 0) because I don't trust all UUID implementations to have + // correctly set those (pycassa doesn't always for instance). + private static final long MIN_CLOCK_SEQ_AND_NODE = 0x8080808080808080L; + private static final long MAX_CLOCK_SEQ_AND_NODE = 0x7f7f7f7f7f7f7f7fL; + + private static final AtomicLong lastTimestamp = new AtomicLong(0L); + + private static long makeEpoch() { + // UUID v1 timestamps must be in 100-nanoseconds interval since 00:00:00.000 15 Oct 1582. + Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT-0")); + c.set(Calendar.YEAR, 1582); + c.set(Calendar.MONTH, Calendar.OCTOBER); + c.set(Calendar.DAY_OF_MONTH, 15); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c.getTimeInMillis(); + } + + private static long makeNode() { + + // We don't have access to the MAC address (in pure JAVA at least) but need to generate a node + // part that identifies this host as uniquely as possible. + // The spec says that one option is to take as many sources that identify this node as possible + // and hash them together. That's what we do here by gathering all the IPs of this host as well + // as a few other sources. + try { + + MessageDigest digest = MessageDigest.getInstance("MD5"); + for (String address : getAllLocalAddresses()) update(digest, address); + + Properties props = System.getProperties(); + update(digest, props.getProperty("java.vendor")); + update(digest, props.getProperty("java.vendor.url")); + update(digest, props.getProperty("java.version")); + update(digest, props.getProperty("os.arch")); + update(digest, props.getProperty("os.name")); + update(digest, props.getProperty("os.version")); + update(digest, getProcessPiece()); + + byte[] hash = digest.digest(); + + long node = 0; + for (int i = 0; i < 6; i++) node |= (0x00000000000000ffL & (long) hash[i]) << (i * 8); + // Since we don't use the MAC address, the spec says that the multicast bit (least significant + // bit of the first byte of the node ID) must be 1. + return node | 0x0000010000000000L; + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private static String getProcessPiece() { + Integer pid = null; + String pidProperty = System.getProperty(PID_SYSTEM_PROPERTY); + if (pidProperty != null) { + try { + pid = Integer.parseInt(pidProperty); + LOG.info("PID obtained from System property {}: {}", PID_SYSTEM_PROPERTY, pid); + } catch (NumberFormatException e) { + LOG.warn( + "Incorrect integer specified for PID in System property {}: {}", + PID_SYSTEM_PROPERTY, + pidProperty); + } + } + if (pid == null && Native.isGetProcessIdAvailable()) { + try { + pid = Native.getProcessId(); + LOG.info("PID obtained through native call to getpid(): {}", pid); + } catch (Exception e) { + Loggers.warnWithException(LOG, "Native call to getpid() failed", e); + } + } + if (pid == null) { + try { + String pidJmx = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; + pid = Integer.parseInt(pidJmx); + LOG.info("PID obtained through JMX: {}", pid); + } catch (Exception e) { + Loggers.warnWithException(LOG, "Failed to obtain PID from JMX", e); + } + } + if (pid == null) { + pid = new Random().nextInt(); + LOG.warn("Could not determine PID, falling back to a random integer: {}", pid); + } + ClassLoader loader = Uuids.class.getClassLoader(); + int loaderId = loader != null ? System.identityHashCode(loader) : 0; + return Integer.toHexString(pid) + Integer.toHexString(loaderId); + } + + private static void update(MessageDigest digest, String value) { + if (value != null) { + digest.update(value.getBytes(Charsets.UTF_8)); + } + } + + private static long makeClockSeqAndNode() { + long clock = new Random(System.currentTimeMillis()).nextLong(); + long node = makeNode(); + + long lsb = 0; + lsb |= (clock & 0x0000000000003FFFL) << 48; + lsb |= 0x8000000000000000L; + lsb |= node; + return lsb; + } + + /** + * Creates a new random (version 4) UUID. + * + *

This method is just a convenience for {@code UUID.randomUUID()}. + */ + @NonNull + public static UUID random() { + return UUID.randomUUID(); + } + + /** + * Creates a new time-based (version 1) UUID. + * + *

UUIDs generated by this method are suitable for use with the {@code timeuuid} Cassandra + * type. In particular the generated UUID includes the timestamp of its generation. + * + *

Note that there is no way to provide your own timestamp. This is deliberate, as we feel that + * this does not conform to the UUID specification, and therefore don't want to encourage it + * through the API. If you want to do it anyway, use the following workaround: + * + *

+   * Random random = new Random();
+   * UUID uuid = new UUID(UUIDs.startOf(userProvidedTimestamp).getMostSignificantBits(), random.nextLong());
+   * 
+ * + * If you simply need to perform a range query on a {@code timeuuid} column, use the "fake" UUID + * generated by {@link #startOf(long)} and {@link #endOf(long)}. + */ + @NonNull + public static UUID timeBased() { + return new UUID(makeMsb(getCurrentTimestamp()), CLOCK_SEQ_AND_NODE); + } + + /** + * Creates a "fake" time-based UUID that sorts as the smallest possible version 1 UUID generated + * at the provided timestamp. + * + *

Such created UUIDs are useful in queries to select a time range of a {@code timeuuid} + * column. + * + *

The UUIDs created by this method are not unique and as such are not suitable + * for anything else than querying a specific time range. In particular, you should not insert + * such UUIDs. "True" UUIDs from user-provided timestamps are not supported (see {@link + * #timeBased()} for more explanations). + * + *

Also, the timestamp to provide as a parameter must be a Unix timestamp (as returned by + * {@link System#currentTimeMillis} or {@link Date#getTime}), and not a count of + * 100-nanosecond intervals since 00:00:00.00, 15 October 1582 (as required by RFC-4122). + * + *

In other words, given a UUID {@code uuid}, you should never call {@code + * startOf(uuid.timestamp())} but rather {@code startOf(unixTimestamp(uuid))}. + * + *

Lastly, please note that Cassandra's {@code timeuuid} sorting is not compatible with {@link + * UUID#compareTo} and hence the UUIDs created by this method are not necessarily lower bound for + * that latter method. + * + * @param timestamp the Unix timestamp for which the created UUID must be a lower bound. + * @return the smallest (for Cassandra {@code timeuuid} sorting) UUID of {@code timestamp}. + */ + @NonNull + public static UUID startOf(long timestamp) { + return new UUID(makeMsb(fromUnixTimestamp(timestamp)), MIN_CLOCK_SEQ_AND_NODE); + } + + /** + * Creates a "fake" time-based UUID that sorts as the biggest possible version 1 UUID generated at + * the provided timestamp. + * + *

See {@link #startOf(long)} for explanations about the intended usage of such UUID. + * + * @param timestamp the Unix timestamp for which the created UUID must be an upper bound. + * @return the biggest (for Cassandra {@code timeuuid} sorting) UUID of {@code timestamp}. + */ + @NonNull + public static UUID endOf(long timestamp) { + long uuidTstamp = fromUnixTimestamp(timestamp + 1) - 1; + return new UUID(makeMsb(uuidTstamp), MAX_CLOCK_SEQ_AND_NODE); + } + + /** + * Returns the Unix timestamp contained by the provided time-based UUID. + * + *

This method is not equivalent to {@link UUID#timestamp()}. More precisely, a version 1 UUID + * stores a timestamp that represents the number of 100-nanoseconds intervals since midnight, 15 + * October 1582 and that is what {@link UUID#timestamp()} returns. This method however converts + * that timestamp to the equivalent Unix timestamp in milliseconds, i.e. a timestamp representing + * a number of milliseconds since midnight, January 1, 1970 UTC. In particular, the timestamps + * returned by this method are comparable to the timestamps returned by {@link + * System#currentTimeMillis}, {@link Date#getTime}, etc. + * + * @throws IllegalArgumentException if {@code uuid} is not a version 1 UUID. + */ + public static long unixTimestamp(@NonNull UUID uuid) { + if (uuid.version() != 1) { + throw new IllegalArgumentException( + String.format( + "Can only retrieve the unix timestamp for version 1 uuid (provided version %d)", + uuid.version())); + } + long timestamp = uuid.timestamp(); + return (timestamp / 10000) + START_EPOCH; + } + + // Use {@link System#currentTimeMillis} for a base time in milliseconds, and if we are in the same + // millisecond as the previous generation, increment the number of nanoseconds. + // However, since the precision is 100-nanosecond intervals, we can only generate 10K UUIDs within + // a millisecond safely. If we detect we have already generated that much UUIDs within a + // millisecond (which, while admittedly unlikely in a real application, is very achievable on even + // modest machines), then we stall the generator (busy spin) until the next millisecond as + // required by the RFC. + private static long getCurrentTimestamp() { + while (true) { + long now = fromUnixTimestamp(System.currentTimeMillis()); + long last = lastTimestamp.get(); + if (now > last) { + if (lastTimestamp.compareAndSet(last, now)) { + return now; + } + } else { + long lastMillis = millisOf(last); + // If the clock went back in time, bail out + if (millisOf(now) < millisOf(last)) { + return lastTimestamp.incrementAndGet(); + } + long candidate = last + 1; + // If we've generated more than 10k uuid in that millisecond, restart the whole process + // until we get to the next millis. Otherwise, we try use our candidate ... unless we've + // been beaten by another thread in which case we try again. + if (millisOf(candidate) == lastMillis && lastTimestamp.compareAndSet(last, candidate)) { + return candidate; + } + } + } + } + + @VisibleForTesting + static long fromUnixTimestamp(long tstamp) { + return (tstamp - START_EPOCH) * 10000; + } + + private static long millisOf(long timestamp) { + return timestamp / 10000; + } + + @VisibleForTesting + static long makeMsb(long timestamp) { + long msb = 0L; + msb |= (0x00000000ffffffffL & timestamp) << 32; + msb |= (0x0000ffff00000000L & timestamp) >>> 16; + msb |= (0x0fff000000000000L & timestamp) >>> 48; + msb |= 0x0000000000001000L; // sets the version to 1. + return msb; + } + + private static Set getAllLocalAddresses() { + Set allIps = new HashSet<>(); + try { + InetAddress localhost = InetAddress.getLocalHost(); + allIps.add(localhost.toString()); + // Also return the hostname if available, it won't hurt (this does a dns lookup, it's only + // done once at startup) + allIps.add(localhost.getCanonicalHostName()); + InetAddress[] allMyIps = InetAddress.getAllByName(localhost.getCanonicalHostName()); + if (allMyIps != null) { + for (InetAddress allMyIp : allMyIps) { + allIps.add(allMyIp.toString()); + } + } + } catch (UnknownHostException e) { + // Ignore, we'll try the network interfaces anyway + } + + try { + Enumeration en = NetworkInterface.getNetworkInterfaces(); + if (en != null) { + while (en.hasMoreElements()) { + Enumeration enumIpAddr = en.nextElement().getInetAddresses(); + while (enumIpAddr.hasMoreElements()) { + allIps.add(enumIpAddr.nextElement().toString()); + } + } + } + } catch (SocketException e) { + // Ignore, if we've really got nothing so far, we'll throw an exception + } + return allIps; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/api/package-info.java b/core/src/main/java/com/datastax/oss/driver/api/package-info.java new file mode 100644 index 00000000000..b783940b313 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/api/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * The driver's public API. + * + *

This package, and all of its subpackages, contains all the types that are intended to be used + * by clients applications. Binary compatibility is guaranteed across minor versions. + */ +package com.datastax.oss.driver.api; diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/AsyncPagingIterableWrapper.java b/core/src/main/java/com/datastax/oss/driver/internal/core/AsyncPagingIterableWrapper.java new file mode 100644 index 00000000000..77490e57416 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/AsyncPagingIterableWrapper.java @@ -0,0 +1,98 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.api.core.AsyncPagingIterable; +import com.datastax.oss.driver.api.core.MappedAsyncPagingIterable; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.shaded.guava.common.collect.AbstractIterator; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Iterator; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +public class AsyncPagingIterableWrapper + implements MappedAsyncPagingIterable { + + private final AsyncPagingIterable source; + private final Function elementMapper; + + private final Iterable currentPage; + + public AsyncPagingIterableWrapper( + AsyncPagingIterable source, + Function elementMapper) { + this.source = source; + this.elementMapper = elementMapper; + + Iterator sourceIterator = source.currentPage().iterator(); + Iterator iterator = + new AbstractIterator() { + @Override + protected TargetT computeNext() { + return (sourceIterator.hasNext()) + ? elementMapper.apply(sourceIterator.next()) + : endOfData(); + } + }; + this.currentPage = () -> iterator; + } + + @NonNull + @Override + public ColumnDefinitions getColumnDefinitions() { + return source.getColumnDefinitions(); + } + + @NonNull + @Override + public ExecutionInfo getExecutionInfo() { + return source.getExecutionInfo(); + } + + @Override + public int remaining() { + return source.remaining(); + } + + @NonNull + @Override + public Iterable currentPage() { + return currentPage; + } + + @Override + public boolean hasMorePages() { + return source.hasMorePages(); + } + + @NonNull + @Override + public CompletionStage> fetchNextPage() + throws IllegalStateException { + return source + .fetchNextPage() + .thenApply( + nextSource -> + new AsyncPagingIterableWrapper(nextSource, elementMapper)); + } + + @Override + public boolean wasApplied() { + return source.wasApplied(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/CassandraProtocolVersionRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/CassandraProtocolVersionRegistry.java new file mode 100644 index 00000000000..f76f2d9b0fa --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/CassandraProtocolVersionRegistry.java @@ -0,0 +1,199 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException; +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.context.DefaultDriverContext; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.Collection; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Optional; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Built-in implementation of the protocol version registry, that supports the protocol versions of + * Apache Cassandra. + * + *

This can be overridden with a custom implementation by subclassing {@link + * DefaultDriverContext}. + * + * @see DefaultProtocolVersion + */ +@ThreadSafe +public class CassandraProtocolVersionRegistry implements ProtocolVersionRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(CassandraProtocolVersionRegistry.class); + private static final ImmutableList values = + ImmutableList.builder().add(DefaultProtocolVersion.values()).build(); + + private final String logPrefix; + private final NavigableMap versionsByCode; + + public CassandraProtocolVersionRegistry(String logPrefix) { + this(logPrefix, DefaultProtocolVersion.values()); + } + + protected CassandraProtocolVersionRegistry(String logPrefix, ProtocolVersion[]... versionRanges) { + this.logPrefix = logPrefix; + this.versionsByCode = byCode(versionRanges); + } + + @Override + public ProtocolVersion fromCode(int code) { + ProtocolVersion protocolVersion = versionsByCode.get(code); + if (protocolVersion == null) { + throw new IllegalArgumentException("Unknown protocol version code: " + code); + } + return protocolVersion; + } + + @Override + public ProtocolVersion fromName(String name) { + for (ProtocolVersion version : versionsByCode.values()) { + if (version.name().equals(name)) { + return version; + } + } + throw new IllegalArgumentException("Unknown protocol version name: " + name); + } + + @Override + public ProtocolVersion highestNonBeta() { + ProtocolVersion highest = versionsByCode.lastEntry().getValue(); + if (!highest.isBeta()) { + return highest; + } else { + return downgrade(highest) + .orElseThrow(() -> new AssertionError("There should be at least one non-beta version")); + } + } + + @Override + public Optional downgrade(ProtocolVersion version) { + Map.Entry previousEntry = + versionsByCode.lowerEntry(version.getCode()); + if (previousEntry == null) { + return Optional.empty(); + } else { + ProtocolVersion previousVersion = previousEntry.getValue(); + // Beta versions are skipped during negotiation + return (previousVersion.isBeta()) ? downgrade(previousVersion) : Optional.of(previousVersion); + } + } + + @Override + public ProtocolVersion highestCommon(Collection nodes) { + if (nodes == null || nodes.isEmpty()) { + throw new IllegalArgumentException("Expected at least one node"); + } + + SortedSet candidates = new TreeSet<>(); + + for (DefaultProtocolVersion version : DefaultProtocolVersion.values()) { + // Beta versions always need to be forced, and we only call this method if the version + // wasn't forced + if (!version.isBeta()) { + candidates.add(version); + } + } + + // The C*<=>protocol mapping is hardcoded in the code below, I don't see a need to be more + // sophisticated right now. + for (Node node : nodes) { + Version version = node.getCassandraVersion(); + if (version == null) { + LOG.warn( + "[{}] Node {} reports null Cassandra version, " + + "ignoring it from optimal protocol version computation", + logPrefix, + node.getEndPoint()); + continue; + } + version = version.nextStable(); + if (version.compareTo(Version.V2_1_0) < 0) { + throw new UnsupportedProtocolVersionException( + node.getEndPoint(), + String.format( + "Node %s reports Cassandra version %s, " + + "but the driver only supports 2.1.0 and above", + node.getEndPoint(), version), + ImmutableList.of(DefaultProtocolVersion.V3, DefaultProtocolVersion.V4)); + } + + LOG.debug( + "[{}] Node {} reports Cassandra version {}", logPrefix, node.getEndPoint(), version); + if (version.compareTo(Version.V2_2_0) < 0 && candidates.remove(DefaultProtocolVersion.V4)) { + LOG.debug("[{}] Excluding protocol V4", logPrefix); + } + } + + if (candidates.isEmpty()) { + // Note: with the current algorithm, this never happens + throw new UnsupportedProtocolVersionException( + null, + String.format( + "Could not determine a common protocol version, " + + "enable DEBUG logs for '%s' for more details", + LOG.getName()), + ImmutableList.of(DefaultProtocolVersion.V3, DefaultProtocolVersion.V4)); + } else { + return candidates.last(); + } + } + + @Override + public boolean supports(ProtocolVersion version, ProtocolFeature feature) { + if (DefaultProtocolFeature.UNSET_BOUND_VALUES.equals(feature)) { + return version.getCode() >= 4; + } else if (DefaultProtocolFeature.PER_REQUEST_KEYSPACE.equals(feature)) { + return version.getCode() >= 5; + } else { + throw new IllegalArgumentException("Unhandled protocol feature: " + feature); + } + } + + @Override + public ImmutableList getValues() { + return values; + } + + private NavigableMap byCode(ProtocolVersion[][] versionRanges) { + NavigableMap map = new TreeMap<>(); + for (ProtocolVersion[] versionRange : versionRanges) { + for (ProtocolVersion version : versionRange) { + ProtocolVersion previous = map.put(version.getCode(), version); + Preconditions.checkArgument( + previous == null, + "Duplicate version code: %s in %s and %s", + version.getCode(), + previous, + version); + } + } + return map; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ConsistencyLevelRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ConsistencyLevelRegistry.java new file mode 100644 index 00000000000..c9353df9b55 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ConsistencyLevelRegistry.java @@ -0,0 +1,34 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; + +/** + * Extension point to plug custom consistency levels. + * + *

This is overridable through {@link InternalDriverContext}. + */ +public interface ConsistencyLevelRegistry { + + ConsistencyLevel codeToLevel(int code); + + int nameToCode(String name); + + /** @return all the values known to this driver instance. */ + Iterable getValues(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java new file mode 100644 index 00000000000..dcbbcac3248 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ContactPoints.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; +import com.datastax.oss.driver.shaded.guava.common.base.Splitter; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.driver.shaded.guava.common.collect.Sets; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Utility class to handle the initial contact points passed to the driver. */ +public class ContactPoints { + private static final Logger LOG = LoggerFactory.getLogger(ContactPoints.class); + + public static Set merge( + Set programmaticContactPoints, List configContactPoints, boolean resolve) { + + Set result = Sets.newHashSet(programmaticContactPoints); + for (String spec : configContactPoints) { + for (InetSocketAddress address : extract(spec, resolve)) { + DefaultEndPoint endPoint = new DefaultEndPoint(address); + boolean wasNew = result.add(endPoint); + if (!wasNew) { + LOG.warn("Duplicate contact point {}", address); + } + } + } + return ImmutableSet.copyOf(result); + } + + private static Set extract(String spec, boolean resolve) { + List hostAndPort = Splitter.on(":").splitToList(spec); + if (hostAndPort.size() != 2) { + LOG.warn("Ignoring invalid contact point {} (expecting host:port)", spec); + return Collections.emptySet(); + } + String host = hostAndPort.get(0); + int port; + try { + port = Integer.parseInt(hostAndPort.get(1)); + } catch (NumberFormatException e) { + LOG.warn( + "Ignoring invalid contact point {} (expecting a number, got {})", + spec, + hostAndPort.get(1)); + return Collections.emptySet(); + } + if (!resolve) { + return ImmutableSet.of(InetSocketAddress.createUnresolved(host, port)); + } else { + try { + InetAddress[] inetAddresses = InetAddress.getAllByName(host); + if (inetAddresses.length > 1) { + LOG.info( + "Contact point {} resolves to multiple addresses, will use them all ({})", + spec, + Arrays.deepToString(inetAddresses)); + } + Set result = new HashSet<>(); + for (InetAddress inetAddress : inetAddresses) { + result.add(new InetSocketAddress(inetAddress, port)); + } + return result; + } catch (UnknownHostException e) { + LOG.warn("Ignoring invalid contact point {} (unknown host {})", spec, host); + return Collections.emptySet(); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/CqlIdentifiers.java b/core/src/main/java/com/datastax/oss/driver/internal/core/CqlIdentifiers.java new file mode 100644 index 00000000000..43b2b2fe249 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/CqlIdentifiers.java @@ -0,0 +1,41 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.List; +import java.util.Map; + +public class CqlIdentifiers { + + public static List wrap(Iterable in) { + ImmutableList.Builder out = ImmutableList.builder(); + for (String name : in) { + out.add(CqlIdentifier.fromCql(name)); + } + return out.build(); + } + + public static Map wrapKeys(Map in) { + ImmutableMap.Builder out = ImmutableMap.builder(); + for (Map.Entry entry : in.entrySet()) { + out.put(CqlIdentifier.fromCql(entry.getKey()), entry.getValue()); + } + return out.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/DefaultConsistencyLevelRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/DefaultConsistencyLevelRegistry.java new file mode 100644 index 00000000000..ba833674292 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/DefaultConsistencyLevelRegistry.java @@ -0,0 +1,53 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DefaultConsistencyLevelRegistry implements ConsistencyLevelRegistry { + + private static final ImmutableList VALUES = + ImmutableList.builder().add(DefaultConsistencyLevel.values()).build(); + private static final ImmutableMap NAME_TO_CODE; + + static { + ImmutableMap.Builder nameToCodeBuilder = ImmutableMap.builder(); + for (DefaultConsistencyLevel consistencyLevel : DefaultConsistencyLevel.values()) { + nameToCodeBuilder.put(consistencyLevel.name(), consistencyLevel.getProtocolCode()); + } + NAME_TO_CODE = nameToCodeBuilder.build(); + } + + @Override + public ConsistencyLevel codeToLevel(int code) { + return DefaultConsistencyLevel.fromCode(code); + } + + @Override + public int nameToCode(String name) { + return NAME_TO_CODE.get(name); + } + + @Override + public Iterable getValues() { + return VALUES; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/DefaultMavenCoordinates.java b/core/src/main/java/com/datastax/oss/driver/internal/core/DefaultMavenCoordinates.java new file mode 100644 index 00000000000..b24a12cb940 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/DefaultMavenCoordinates.java @@ -0,0 +1,102 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.api.core.MavenCoordinates; +import com.datastax.oss.driver.api.core.Version; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.buffer.ByteBuf; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DefaultMavenCoordinates implements MavenCoordinates { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultMavenCoordinates.class); + + public static MavenCoordinates buildFromResourceAndPrint(URL resource) { + MavenCoordinates info = buildFromResource(resource); + LOG.info("{}", info); + return info; + } + + public static DefaultMavenCoordinates buildFromResource(URL resource) { + // The resource is assumed to be a properties file, but + // encoded in UTF-8, not ISO-8859-1 as required by the Java specs, + // since our build tool (Maven) produces UTF-8-encoded resources. + try (InputStreamReader reader = + new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8)) { + Properties props = new Properties(); + props.load(reader); + String name = props.getProperty("driver.name"); + String groupId = props.getProperty("driver.groupId"); + String artifactId = props.getProperty("driver.artifactId"); + String version = props.getProperty("driver.version"); + if (ByteBuf.class.getPackage().getName().contains("com.datastax.oss.driver.shaded")) { + version += "-shaded"; + } + return new DefaultMavenCoordinates(name, groupId, artifactId, Version.parse(version)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private final String name; + private final String groupId; + private final String artifactId; + private final Version version; + + public DefaultMavenCoordinates(String name, String groupId, String artifactId, Version version) { + this.name = name; + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + } + + @NonNull + @Override + public String getName() { + return name; + } + + @NonNull + @Override + public String getGroupId() { + return groupId; + } + + @NonNull + @Override + public String getArtifactId() { + return artifactId; + } + + @NonNull + @Override + public Version getVersion() { + return version; + } + + @Override + public String toString() { + return String.format("%s (%s:%s) version %s", name, groupId, artifactId, version); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/DefaultProtocolFeature.java b/core/src/main/java/com/datastax/oss/driver/internal/core/DefaultProtocolFeature.java new file mode 100644 index 00000000000..8d26d1d23f4 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/DefaultProtocolFeature.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +/** + * Features that are commonly supported by most Apache Cassandra protocol versions. + * + * @see com.datastax.oss.driver.api.core.DefaultProtocolVersion + */ +public enum DefaultProtocolFeature implements ProtocolFeature { + + /** + * The ability to leave variables unset in prepared statements. + * + * @see CASSANDRA-7304 + */ + UNSET_BOUND_VALUES, + + /** + * The ability to override the keyspace on a per-request basis. + * + * @see CASSANDRA-10145 + */ + PER_REQUEST_KEYSPACE, + ; +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/PagingIterableWrapper.java b/core/src/main/java/com/datastax/oss/driver/internal/core/PagingIterableWrapper.java new file mode 100644 index 00000000000..675f9f2cd4b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/PagingIterableWrapper.java @@ -0,0 +1,78 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.api.core.PagingIterable; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.shaded.guava.common.collect.AbstractIterator; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; + +public class PagingIterableWrapper implements PagingIterable { + + private final PagingIterable source; + private final Iterator iterator; + + public PagingIterableWrapper( + PagingIterable source, Function elementMapper) { + this.source = source; + Iterator sourceIterator = source.iterator(); + this.iterator = + new AbstractIterator() { + @Override + protected TargetT computeNext() { + return (sourceIterator.hasNext()) + ? elementMapper.apply(sourceIterator.next()) + : endOfData(); + } + }; + } + + @NonNull + @Override + public ColumnDefinitions getColumnDefinitions() { + return source.getColumnDefinitions(); + } + + @NonNull + @Override + public List getExecutionInfos() { + return source.getExecutionInfos(); + } + + @Override + public boolean isFullyFetched() { + return source.isFullyFetched(); + } + + @Override + public int getAvailableWithoutFetching() { + return source.getAvailableWithoutFetching(); + } + + @Override + public boolean wasApplied() { + return source.wasApplied(); + } + + @Override + public Iterator iterator() { + return iterator; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ProtocolFeature.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ProtocolFeature.java new file mode 100644 index 00000000000..7f4e286ea17 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ProtocolFeature.java @@ -0,0 +1,31 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.api.core.ProtocolVersion; + +/** + * A marker interface for features of the native protocol that are only supported by specific + * {@linkplain ProtocolVersion versions}. + * + *

The only reason to model this as an interface (as opposed to an enum type) is to accommodate + * for custom protocol extensions. If you're connecting to a standard Apache Cassandra cluster, all + * {@code ProtocolFeature}s are {@link DefaultProtocolFeature} instances. + * + * @see ProtocolVersionRegistry#supports(ProtocolVersion, ProtocolFeature) + * @see DefaultProtocolFeature + */ +public interface ProtocolFeature {} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ProtocolVersionRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ProtocolVersionRegistry.java new file mode 100644 index 00000000000..2f3c3b9a972 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ProtocolVersionRegistry.java @@ -0,0 +1,75 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.metadata.TopologyMonitor; +import java.util.Collection; +import java.util.Optional; + +/** Defines which native protocol versions are supported by a driver instance. */ +public interface ProtocolVersionRegistry { + + /** + * Look up a version by its {@link ProtocolVersion#getCode()} code}. + * + * @throws IllegalArgumentException if there is no known version with this code. + */ + ProtocolVersion fromCode(int code); + + /** + * Look up a version by its {@link ProtocolVersion#name() name}. This is used when a version was + * forced in the configuration. + * + * @throws IllegalArgumentException if there is no known version with this name. + * @see DefaultDriverOption#PROTOCOL_VERSION + */ + ProtocolVersion fromName(String name); + + /** + * The highest, non-beta version supported by the driver. This is used as the starting point for + * the negotiation process for the initial connection (if the version wasn't forced). + */ + ProtocolVersion highestNonBeta(); + + /** + * Downgrade to a lower version if the current version is not supported by the server. This is + * used during the negotiation process for the initial connection (if the version wasn't forced). + * + * @return empty if there is no version to downgrade to. + */ + Optional downgrade(ProtocolVersion version); + + /** + * Computes the highest common version supported by the given nodes. This is called after the + * initial {@link TopologyMonitor#refreshNodeList()} node refresh} (provided that the version was + * not forced), to ensure that we proceed with a version that will work with all the nodes. + * + * @throws UnsupportedProtocolVersionException if no such version exists (the nodes support + * non-intersecting ranges), or if there was an error during the computation. This will cause + * the driver initialization to fail. + */ + ProtocolVersion highestCommon(Collection nodes); + + /** Whether a given version supports a given feature. */ + boolean supports(ProtocolVersion version, ProtocolFeature feature); + + /** @return all the values known to this driver instance. */ + Iterable getValues(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/Ec2MultiRegionAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/Ec2MultiRegionAddressTranslator.java new file mode 100644 index 00000000000..055da787381 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/Ec2MultiRegionAddressTranslator.java @@ -0,0 +1,158 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.addresstranslation; + +import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Enumeration; +import java.util.Hashtable; +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link AddressTranslator} implementation for a multi-region EC2 deployment where clients are + * also deployed in EC2. + * + *

Its distinctive feature is that it translates addresses according to the location of the + * Cassandra host: + * + *

    + *
  • addresses in different EC2 regions (than the client) are unchanged; + *
  • addresses in the same EC2 region are translated to private IPs. + *
+ * + * This optimizes network costs, because Amazon charges more for communication over public IPs. + * + *

Implementation note: this class performs a reverse DNS lookup of the origin address, to find + * the domain name of the target instance. Then it performs a forward DNS lookup of the domain name; + * the EC2 DNS does the private/public switch automatically based on location. + */ +public class Ec2MultiRegionAddressTranslator implements AddressTranslator { + + private static final Logger LOG = LoggerFactory.getLogger(Ec2MultiRegionAddressTranslator.class); + + private final DirContext ctx; + private final String logPrefix; + + public Ec2MultiRegionAddressTranslator( + @SuppressWarnings("unused") @NonNull DriverContext context) { + this.logPrefix = context.getSessionName(); + @SuppressWarnings("JdkObsolete") + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); + try { + ctx = new InitialDirContext(env); + } catch (NamingException e) { + throw new RuntimeException("Could not create translator", e); + } + } + + @VisibleForTesting + Ec2MultiRegionAddressTranslator(@NonNull DirContext ctx) { + this.logPrefix = "test"; + this.ctx = ctx; + } + + @NonNull + @Override + public InetSocketAddress translate(@NonNull InetSocketAddress socketAddress) { + InetAddress address = socketAddress.getAddress(); + try { + // InetAddress#getHostName() is supposed to perform a reverse DNS lookup, but for some reason + // it doesn't work within the same EC2 region (it returns the IP address itself). + // We use an alternate implementation: + String domainName = lookupPtrRecord(reverse(address)); + if (domainName == null) { + LOG.warn("[{}] Found no domain name for {}, returning it as-is", logPrefix, address); + return socketAddress; + } + + InetAddress translatedAddress = InetAddress.getByName(domainName); + LOG.debug("[{}] Resolved {} to {}", logPrefix, address, translatedAddress); + return new InetSocketAddress(translatedAddress, socketAddress.getPort()); + } catch (Exception e) { + Loggers.warnWithException( + LOG, "[{}] Error resolving {}, returning it as-is", logPrefix, address, e); + return socketAddress; + } + } + + private String lookupPtrRecord(String reversedDomain) throws Exception { + Attributes attrs = ctx.getAttributes(reversedDomain, new String[] {"PTR"}); + for (NamingEnumeration ae = attrs.getAll(); ae.hasMoreElements(); ) { + Attribute attr = (Attribute) ae.next(); + Enumeration vals = attr.getAll(); + if (vals.hasMoreElements()) { + return vals.nextElement().toString(); + } + } + return null; + } + + @Override + public void close() { + try { + ctx.close(); + } catch (NamingException e) { + Loggers.warnWithException(LOG, "Error closing translator", e); + } + } + + // Builds the "reversed" domain name in the ARPA domain to perform the reverse lookup + @VisibleForTesting + static String reverse(InetAddress address) { + byte[] bytes = address.getAddress(); + if (bytes.length == 4) return reverseIpv4(bytes); + else return reverseIpv6(bytes); + } + + private static String reverseIpv4(byte[] bytes) { + StringBuilder builder = new StringBuilder(); + for (int i = bytes.length - 1; i >= 0; i--) { + builder.append(bytes[i] & 0xFF).append('.'); + } + builder.append("in-addr.arpa"); + return builder.toString(); + } + + private static String reverseIpv6(byte[] bytes) { + StringBuilder builder = new StringBuilder(); + for (int i = bytes.length - 1; i >= 0; i--) { + byte b = bytes[i]; + int lowNibble = b & 0x0F; + int highNibble = b >> 4 & 0x0F; + builder + .append(Integer.toHexString(lowNibble)) + .append('.') + .append(Integer.toHexString(highNibble)) + .append('.'); + } + builder.append("ip6.arpa"); + return builder.toString(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/PassThroughAddressTranslator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/PassThroughAddressTranslator.java new file mode 100644 index 00000000000..7628d6a0eda --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/addresstranslation/PassThroughAddressTranslator.java @@ -0,0 +1,57 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.addresstranslation; + +import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; +import com.datastax.oss.driver.api.core.context.DriverContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetSocketAddress; +import net.jcip.annotations.ThreadSafe; + +/** + * An address translator that always returns the same address unchanged. + * + *

To activate this translator, modify the {@code advanced.address-translator} section in the + * driver configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.address-translator {
+ *     class = PassThroughAddressTranslator
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class PassThroughAddressTranslator implements AddressTranslator { + + public PassThroughAddressTranslator(@SuppressWarnings("unused") DriverContext context) { + // nothing to do + } + + @NonNull + @Override + public InetSocketAddress translate(@NonNull InetSocketAddress address) { + return address; + } + + @Override + public void close() { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/AdminRequestHandler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/AdminRequestHandler.java new file mode 100644 index 00000000000..6ccf1651e1f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/AdminRequestHandler.java @@ -0,0 +1,241 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.adminrequest; + +import com.datastax.oss.driver.api.core.DriverTimeoutException; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.ResponseCallback; +import com.datastax.oss.driver.internal.core.util.concurrent.UncaughtExceptions; +import com.datastax.oss.driver.shaded.guava.common.collect.Maps; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.request.Query; +import com.datastax.oss.protocol.internal.request.query.QueryOptions; +import com.datastax.oss.protocol.internal.response.Result; +import com.datastax.oss.protocol.internal.response.result.Rows; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ScheduledFuture; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Handles the lifecyle of an admin request (such as a node refresh or schema refresh query). */ +@ThreadSafe +public class AdminRequestHandler implements ResponseCallback { + private static final Logger LOG = LoggerFactory.getLogger(AdminRequestHandler.class); + + public static AdminRequestHandler query( + DriverChannel channel, Query query, Duration timeout, String logPrefix) { + return createAdminRequestHandler(channel, query, Collections.emptyMap(), timeout, logPrefix); + } + + public static AdminRequestHandler query( + DriverChannel channel, + String query, + Map parameters, + Duration timeout, + int pageSize, + String logPrefix) { + Query message = + new Query( + query, + buildQueryOptions(pageSize, serialize(parameters, channel.protocolVersion()), null)); + return createAdminRequestHandler(channel, message, parameters, timeout, logPrefix); + } + + private static AdminRequestHandler createAdminRequestHandler( + DriverChannel channel, + Query message, + Map parameters, + Duration timeout, + String logPrefix) { + + String debugString = "query '" + message.query + "'"; + if (!parameters.isEmpty()) { + debugString += " with parameters " + parameters; + } + return new AdminRequestHandler( + channel, message, Frame.NO_PAYLOAD, timeout, logPrefix, debugString); + } + + public static AdminRequestHandler query( + DriverChannel channel, String query, Duration timeout, int pageSize, String logPrefix) { + return query(channel, query, Collections.emptyMap(), timeout, pageSize, logPrefix); + } + + private final DriverChannel channel; + private final Message message; + private final Map customPayload; + private final Duration timeout; + private final String logPrefix; + private final String debugString; + protected final CompletableFuture result = new CompletableFuture<>(); + + // This is only ever accessed on the channel's event loop, so it doesn't need to be volatile + private ScheduledFuture timeoutFuture; + + public AdminRequestHandler( + DriverChannel channel, + Message message, + Map customPayload, + Duration timeout, + String logPrefix, + String debugString) { + this.channel = channel; + this.message = message; + this.customPayload = customPayload; + this.timeout = timeout; + this.logPrefix = logPrefix; + this.debugString = debugString; + } + + public CompletionStage start() { + LOG.debug("[{}] Executing {}", logPrefix, this); + channel.write(message, false, customPayload, this).addListener(this::onWriteComplete); + return result; + } + + private void onWriteComplete(Future future) { + if (future.isSuccess()) { + LOG.debug("[{}] Successfully wrote {}, waiting for response", logPrefix, this); + if (timeout.toNanos() > 0) { + timeoutFuture = + channel + .eventLoop() + .schedule(this::fireTimeout, timeout.toNanos(), TimeUnit.NANOSECONDS); + timeoutFuture.addListener(UncaughtExceptions::log); + } + } else { + setFinalError(future.cause()); + } + } + + private void fireTimeout() { + setFinalError( + new DriverTimeoutException(String.format("%s timed out after %s", debugString, timeout))); + if (!channel.closeFuture().isDone()) { + channel.cancel(this); + } + } + + @Override + public void onFailure(Throwable error) { + if (timeoutFuture != null) { + timeoutFuture.cancel(true); + } + setFinalError(error); + } + + @Override + public void onResponse(Frame responseFrame) { + if (timeoutFuture != null) { + timeoutFuture.cancel(true); + } + Message message = responseFrame.message; + LOG.debug("[{}] Got response {}", logPrefix, responseFrame.message); + if (message instanceof Rows) { + Rows rows = (Rows) message; + ByteBuffer pagingState = rows.getMetadata().pagingState; + AdminRequestHandler nextHandler = (pagingState == null) ? null : this.copy(pagingState); + setFinalResult(new AdminResult(rows, nextHandler, channel.protocolVersion())); + } else if (message instanceof Result) { + + // Internal prepares are only "reprepare on up" types of queries, where we only care about + // success, not the actual result, so this is good enough: + setFinalResult(null); + } else { + setFinalError(new UnexpectedResponseException(debugString, message)); + } + } + + protected boolean setFinalResult(AdminResult result) { + return this.result.complete(result); + } + + protected boolean setFinalError(Throwable error) { + return result.completeExceptionally(error); + } + + private AdminRequestHandler copy(ByteBuffer pagingState) { + assert message instanceof Query; + Query current = (Query) this.message; + QueryOptions currentOptions = current.options; + QueryOptions newOptions = + buildQueryOptions(currentOptions.pageSize, currentOptions.namedValues, pagingState); + return new AdminRequestHandler( + channel, + new Query(current.query, newOptions), + customPayload, + timeout, + logPrefix, + debugString); + } + + private static QueryOptions buildQueryOptions( + int pageSize, Map serialize, ByteBuffer pagingState) { + return new QueryOptions( + ProtocolConstants.ConsistencyLevel.ONE, + Collections.emptyList(), + serialize, + false, + pageSize, + pagingState, + ProtocolConstants.ConsistencyLevel.SERIAL, + Long.MIN_VALUE, + null); + } + + private static Map serialize( + Map parameters, ProtocolVersion protocolVersion) { + Map result = Maps.newHashMapWithExpectedSize(parameters.size()); + for (Map.Entry entry : parameters.entrySet()) { + result.put(entry.getKey(), serialize(entry.getValue(), protocolVersion)); + } + return result; + } + + private static ByteBuffer serialize(Object parameter, ProtocolVersion protocolVersion) { + if (parameter instanceof String) { + return TypeCodecs.TEXT.encode((String) parameter, protocolVersion); + } else if (parameter instanceof InetAddress) { + return TypeCodecs.INET.encode((InetAddress) parameter, protocolVersion); + } else if (parameter instanceof List && ((List) parameter).get(0) instanceof String) { + @SuppressWarnings("unchecked") + List l = (List) parameter; + return AdminRow.LIST_OF_TEXT.encode(l, protocolVersion); + } else { + throw new IllegalArgumentException( + "Unsupported variable type for admin query: " + parameter.getClass()); + } + } + + @Override + public String toString() { + return debugString; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/AdminResult.java b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/AdminResult.java new file mode 100644 index 00000000000..d40a85049fc --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/AdminResult.java @@ -0,0 +1,81 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.adminrequest; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.collect.AbstractIterator; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.protocol.internal.response.result.ColumnSpec; +import com.datastax.oss.protocol.internal.response.result.Rows; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CompletionStage; +import net.jcip.annotations.NotThreadSafe; + +@NotThreadSafe // wraps a mutable queue +public class AdminResult implements Iterable { + + private final Queue> data; + private final Map columnSpecs; + private final AdminRequestHandler nextHandler; + private final ProtocolVersion protocolVersion; + + public AdminResult(Rows rows, AdminRequestHandler nextHandler, ProtocolVersion protocolVersion) { + this.data = rows.getData(); + + ImmutableMap.Builder columnSpecsBuilder = ImmutableMap.builder(); + for (ColumnSpec spec : rows.getMetadata().columnSpecs) { + columnSpecsBuilder.put(spec.name, spec); + } + // Admin queries are simple selects only, so there are no duplicate names (if that ever + // changes, build() will fail and we'll have to do things differently) + this.columnSpecs = columnSpecsBuilder.build(); + + this.nextHandler = nextHandler; + this.protocolVersion = protocolVersion; + } + + /** This consumes the result's data and can be called only once. */ + @NonNull + @Override + public Iterator iterator() { + return new AbstractIterator() { + @Override + protected AdminRow computeNext() { + List rowData = data.poll(); + return (rowData == null) + ? endOfData() + : new AdminRow(columnSpecs, rowData, protocolVersion); + } + }; + } + + public boolean hasNextPage() { + return nextHandler != null; + } + + public CompletionStage nextPage() { + return (nextHandler == null) + ? CompletableFutures.failedFuture( + new AssertionError("No next page, use hasNextPage() before you call this method")) + : nextHandler.start(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/AdminRow.java b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/AdminRow.java new file mode 100644 index 00000000000..900002ddab2 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/AdminRow.java @@ -0,0 +1,107 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.adminrequest; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.result.ColumnSpec; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import net.jcip.annotations.Immutable; + +@Immutable +public class AdminRow { + + @VisibleForTesting + static final TypeCodec> LIST_OF_TEXT = TypeCodecs.listOf(TypeCodecs.TEXT); + + private static final TypeCodec> SET_OF_TEXT = TypeCodecs.setOf(TypeCodecs.TEXT); + private static final TypeCodec> MAP_OF_STRING_TO_STRING = + TypeCodecs.mapOf(TypeCodecs.TEXT, TypeCodecs.TEXT); + + private final Map columnSpecs; + private final List data; + private final ProtocolVersion protocolVersion; + + public AdminRow( + Map columnSpecs, List data, ProtocolVersion protocolVersion) { + this.columnSpecs = columnSpecs; + this.data = data; + this.protocolVersion = protocolVersion; + } + + public Boolean getBoolean(String columnName) { + return get(columnName, TypeCodecs.BOOLEAN); + } + + public Integer getInteger(String columnName) { + return get(columnName, TypeCodecs.INT); + } + + public boolean isString(String columnName) { + return columnSpecs.get(columnName).type.id == ProtocolConstants.DataType.VARCHAR; + } + + public String getString(String columnName) { + return get(columnName, TypeCodecs.TEXT); + } + + public UUID getUuid(String columnName) { + return get(columnName, TypeCodecs.UUID); + } + + public ByteBuffer getByteBuffer(String columnName) { + return get(columnName, TypeCodecs.BLOB); + } + + public InetAddress getInetAddress(String columnName) { + return get(columnName, TypeCodecs.INET); + } + + public List getListOfString(String columnName) { + return get(columnName, LIST_OF_TEXT); + } + + public Set getSetOfString(String columnName) { + return get(columnName, SET_OF_TEXT); + } + + public Map getMapOfStringToString(String columnName) { + return get(columnName, MAP_OF_STRING_TO_STRING); + } + + public boolean contains(String columnName) { + return columnSpecs.containsKey(columnName); + } + + public T get(String columnName, TypeCodec codec) { + // Minimal checks here: this is for internal use, so the caller should know what they're + // doing + if (!contains(columnName)) { + return null; + } else { + int index = columnSpecs.get(columnName).index; + return codec.decode(data.get(index), protocolVersion); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/ThrottledAdminRequestHandler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/ThrottledAdminRequestHandler.java new file mode 100644 index 00000000000..a4d9809a32d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/ThrottledAdminRequestHandler.java @@ -0,0 +1,102 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.adminrequest; + +import com.datastax.oss.driver.api.core.DriverTimeoutException; +import com.datastax.oss.driver.api.core.RequestThrottlingException; +import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.api.core.session.throttling.Throttled; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.metrics.SessionMetricUpdater; +import com.datastax.oss.protocol.internal.Message; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class ThrottledAdminRequestHandler extends AdminRequestHandler implements Throttled { + + private final long startTimeNanos; + private final RequestThrottler throttler; + private final SessionMetricUpdater metricUpdater; + + public ThrottledAdminRequestHandler( + DriverChannel channel, + Message message, + Map customPayload, + Duration timeout, + RequestThrottler throttler, + SessionMetricUpdater metricUpdater, + String logPrefix, + String debugString) { + super(channel, message, customPayload, timeout, logPrefix, debugString); + this.startTimeNanos = System.nanoTime(); + this.throttler = throttler; + this.metricUpdater = metricUpdater; + } + + @Override + public CompletionStage start() { + // Don't write request yet, wait for green light from throttler + throttler.register(this); + return result; + } + + @Override + public void onThrottleReady(boolean wasDelayed) { + if (wasDelayed) { + metricUpdater.updateTimer( + DefaultSessionMetric.THROTTLING_DELAY, + null, + System.nanoTime() - startTimeNanos, + TimeUnit.NANOSECONDS); + } + super.start(); + } + + @Override + public void onThrottleFailure(@NonNull RequestThrottlingException error) { + metricUpdater.incrementCounter(DefaultSessionMetric.THROTTLING_ERRORS, null); + setFinalError(error); + } + + @Override + protected boolean setFinalResult(AdminResult result) { + boolean wasSet = super.setFinalResult(result); + if (wasSet) { + throttler.signalSuccess(this); + } + return wasSet; + } + + @Override + protected boolean setFinalError(Throwable error) { + boolean wasSet = super.setFinalError(error); + if (wasSet) { + if (error instanceof DriverTimeoutException) { + throttler.signalTimeout(this); + } else if (!(error instanceof RequestThrottlingException)) { + throttler.signalError(this, error); + } + } + return wasSet; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/UnexpectedResponseException.java b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/UnexpectedResponseException.java new file mode 100644 index 00000000000..d475a4400db --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/UnexpectedResponseException.java @@ -0,0 +1,28 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.adminrequest; + +import com.datastax.oss.protocol.internal.Message; + +public class UnexpectedResponseException extends Exception { + + public final Message message; + + UnexpectedResponseException(String requestName, Message message) { + super(String.format("%s got unexpected response %s", requestName, message)); + this.message = message; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/package-info.java b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/package-info.java new file mode 100644 index 00000000000..e12c4db8514 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/adminrequest/package-info.java @@ -0,0 +1,34 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Infrastructure to execute internal requests in the driver, for example control connection + * queries, or automatic statement preparation. + * + *

This is a stripped-down version of the public API, with the bare minimum for our needs: + * + *

    + *
  • async mode only. + *
  • execution on a given channel, without retries. + *
  • {@code QUERY} and {@code PREPARE} messages only. + *
  • paging is possible, but only on the same channel. If the channel gets closed between pages, + * the query fails. + *
  • values can only be bound by name, and it is assumed that the target type can always be + * inferred unambiguously (i.e. the only integer type is {@code int}, etc). + *
  • limited result API: getters by internal name only, no custom codecs. + *
  • codecs are only implemented for the types we actually need for admin queries. + *
+ */ +package com.datastax.oss.driver.internal.core.adminrequest; diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/auth/PlainTextAuthProvider.java b/core/src/main/java/com/datastax/oss/driver/internal/core/auth/PlainTextAuthProvider.java new file mode 100644 index 00000000000..cf0bbfcb917 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/auth/PlainTextAuthProvider.java @@ -0,0 +1,118 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.auth; + +import com.datastax.oss.driver.api.core.auth.AuthProvider; +import com.datastax.oss.driver.api.core.auth.Authenticator; +import com.datastax.oss.driver.api.core.auth.SyncAuthenticator; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.shaded.guava.common.base.Charsets; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A simple authentication provider that supports SASL authentication using the PLAIN mechanism for + * version 3 (or above) of the CQL native protocol. + * + *

To activate this provider, add an {@code advanced.auth-provider} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.auth-provider {
+ *     class = com.datastax.driver.api.core.auth.PlainTextAuthProvider
+ *     username = cassandra
+ *     password = cassandra
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class PlainTextAuthProvider implements AuthProvider { + + private static final Logger LOG = LoggerFactory.getLogger(PlainTextAuthProvider.class); + + private final String logPrefix; + private final DriverExecutionProfile config; + + /** Builds a new instance. */ + public PlainTextAuthProvider(DriverContext context) { + this.logPrefix = context.getSessionName(); + this.config = context.getConfig().getDefaultProfile(); + } + + @NonNull + @Override + public Authenticator newAuthenticator( + @NonNull EndPoint endPoint, @NonNull String serverAuthenticator) { + String username = config.getString(DefaultDriverOption.AUTH_PROVIDER_USER_NAME); + String password = config.getString(DefaultDriverOption.AUTH_PROVIDER_PASSWORD); + return new PlainTextAuthenticator(username, password); + } + + @Override + public void onMissingChallenge(@NonNull EndPoint endPoint) { + LOG.warn( + "[{}] {} did not send an authentication challenge; " + + "This is suspicious because the driver expects authentication", + logPrefix, + endPoint); + } + + @Override + public void close() throws Exception { + // nothing to do + } + + private static class PlainTextAuthenticator implements SyncAuthenticator { + + private final ByteBuffer initialToken; + + PlainTextAuthenticator(String username, String password) { + byte[] usernameBytes = username.getBytes(Charsets.UTF_8); + byte[] passwordBytes = password.getBytes(Charsets.UTF_8); + this.initialToken = ByteBuffer.allocate(usernameBytes.length + passwordBytes.length + 2); + initialToken.put((byte) 0); + initialToken.put(usernameBytes); + initialToken.put((byte) 0); + initialToken.put(passwordBytes); + initialToken.flip(); + } + + @Override + public ByteBuffer initialResponseSync() { + return initialToken.duplicate(); + } + + @Override + public ByteBuffer evaluateChallengeSync(ByteBuffer token) { + return null; + } + + @Override + public void onAuthenticationSuccessSync(ByteBuffer token) { + // no-op, the server should send nothing anyway + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ChannelEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ChannelEvent.java new file mode 100644 index 00000000000..a2cb5d35956 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ChannelEvent.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.metadata.Node; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +/** Events relating to driver channels. */ +@Immutable +public class ChannelEvent { + public enum Type { + OPENED, + CLOSED, + RECONNECTION_STARTED, + RECONNECTION_STOPPED, + CONTROL_CONNECTION_FAILED + } + + public static ChannelEvent channelOpened(Node node) { + return new ChannelEvent(Type.OPENED, node); + } + + public static ChannelEvent channelClosed(Node node) { + return new ChannelEvent(Type.CLOSED, node); + } + + public static ChannelEvent reconnectionStarted(Node node) { + return new ChannelEvent(Type.RECONNECTION_STARTED, node); + } + + public static ChannelEvent reconnectionStopped(Node node) { + return new ChannelEvent(Type.RECONNECTION_STOPPED, node); + } + + /** The control connection tried to use this node, but failed to open a channel. */ + public static ChannelEvent controlConnectionFailed(Node node) { + return new ChannelEvent(Type.CONTROL_CONNECTION_FAILED, node); + } + + public final Type type; + public final Node node; + + public ChannelEvent(Type type, Node node) { + this.type = type; + this.node = node; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof ChannelEvent) { + ChannelEvent that = (ChannelEvent) other; + return this.type == that.type && Objects.equals(this.node, that.node); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(type, node); + } + + @Override + public String toString() { + return "ChannelEvent(" + type + ", " + node + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ChannelFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ChannelFactory.java new file mode 100644 index 00000000000..9d5caa03b8f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ChannelFactory.java @@ -0,0 +1,312 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.metrics.NodeMetricUpdater; +import com.datastax.oss.driver.internal.core.metrics.NoopNodeMetricUpdater; +import com.datastax.oss.driver.internal.core.metrics.SessionMetricUpdater; +import com.datastax.oss.driver.internal.core.protocol.FrameDecoder; +import com.datastax.oss.driver.internal.core.protocol.FrameEncoder; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.FixedRecvByteBufAllocator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Builds {@link DriverChannel} objects for an instance of the driver. */ +@ThreadSafe +public class ChannelFactory { + + private static final Logger LOG = LoggerFactory.getLogger(ChannelFactory.class); + + private final String logPrefix; + protected final InternalDriverContext context; + + /** either set from the configuration, or null and will be negotiated */ + @VisibleForTesting ProtocolVersion protocolVersion; + + @VisibleForTesting volatile String clusterName; + + public ChannelFactory(InternalDriverContext context) { + this.logPrefix = context.getSessionName(); + this.context = context; + + DriverExecutionProfile defaultConfig = context.getConfig().getDefaultProfile(); + if (defaultConfig.isDefined(DefaultDriverOption.PROTOCOL_VERSION)) { + String versionName = defaultConfig.getString(DefaultDriverOption.PROTOCOL_VERSION); + this.protocolVersion = context.getProtocolVersionRegistry().fromName(versionName); + } // else it will be negotiated with the first opened connection + } + + public ProtocolVersion getProtocolVersion() { + ProtocolVersion result = this.protocolVersion; + Preconditions.checkState( + result != null, "Protocol version not known yet, this should only be called after init"); + return result; + } + + /** + * WARNING: this is only used at the very beginning of the init process (when we just refreshed + * the list of nodes for the first time, and found out that one of them requires a lower version + * than was negotiated with the first contact point); it's safe at this time because we are in a + * controlled state (only the control connection is open, it's not executing queries and we're + * going to reconnect immediately after). Calling this method at any other time will likely wreak + * havoc. + */ + public void setProtocolVersion(ProtocolVersion newVersion) { + this.protocolVersion = newVersion; + } + + public CompletionStage connect(Node node, DriverChannelOptions options) { + NodeMetricUpdater nodeMetricUpdater; + if (node instanceof DefaultNode) { + nodeMetricUpdater = ((DefaultNode) node).getMetricUpdater(); + } else { + nodeMetricUpdater = NoopNodeMetricUpdater.INSTANCE; + } + return connect(node.getEndPoint(), options, nodeMetricUpdater); + } + + @VisibleForTesting + CompletionStage connect( + EndPoint endPoint, DriverChannelOptions options, NodeMetricUpdater nodeMetricUpdater) { + CompletableFuture resultFuture = new CompletableFuture<>(); + + ProtocolVersion currentVersion; + boolean isNegotiating; + List attemptedVersions = new CopyOnWriteArrayList<>(); + if (this.protocolVersion != null) { + currentVersion = protocolVersion; + isNegotiating = false; + } else { + currentVersion = context.getProtocolVersionRegistry().highestNonBeta(); + isNegotiating = true; + } + + connect( + endPoint, + options, + nodeMetricUpdater, + currentVersion, + isNegotiating, + attemptedVersions, + resultFuture); + return resultFuture; + } + + private void connect( + EndPoint endPoint, + DriverChannelOptions options, + NodeMetricUpdater nodeMetricUpdater, + ProtocolVersion currentVersion, + boolean isNegotiating, + List attemptedVersions, + CompletableFuture resultFuture) { + + NettyOptions nettyOptions = context.getNettyOptions(); + + Bootstrap bootstrap = + new Bootstrap() + .group(nettyOptions.ioEventLoopGroup()) + .channel(nettyOptions.channelClass()) + .option(ChannelOption.ALLOCATOR, nettyOptions.allocator()) + .handler( + initializer(endPoint, currentVersion, options, nodeMetricUpdater, resultFuture)); + + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + + boolean tcpNoDelay = config.getBoolean(DefaultDriverOption.SOCKET_TCP_NODELAY); + bootstrap = bootstrap.option(ChannelOption.TCP_NODELAY, tcpNoDelay); + if (config.isDefined(DefaultDriverOption.SOCKET_KEEP_ALIVE)) { + boolean keepAlive = config.getBoolean(DefaultDriverOption.SOCKET_KEEP_ALIVE); + bootstrap = bootstrap.option(ChannelOption.SO_KEEPALIVE, keepAlive); + } + if (config.isDefined(DefaultDriverOption.SOCKET_REUSE_ADDRESS)) { + boolean reuseAddress = config.getBoolean(DefaultDriverOption.SOCKET_REUSE_ADDRESS); + bootstrap = bootstrap.option(ChannelOption.SO_REUSEADDR, reuseAddress); + } + if (config.isDefined(DefaultDriverOption.SOCKET_LINGER_INTERVAL)) { + int lingerInterval = config.getInt(DefaultDriverOption.SOCKET_LINGER_INTERVAL); + bootstrap = bootstrap.option(ChannelOption.SO_LINGER, lingerInterval); + } + if (config.isDefined(DefaultDriverOption.SOCKET_RECEIVE_BUFFER_SIZE)) { + int receiveBufferSize = config.getInt(DefaultDriverOption.SOCKET_RECEIVE_BUFFER_SIZE); + bootstrap = + bootstrap + .option(ChannelOption.SO_RCVBUF, receiveBufferSize) + .option( + ChannelOption.RCVBUF_ALLOCATOR, new FixedRecvByteBufAllocator(receiveBufferSize)); + } + if (config.isDefined(DefaultDriverOption.SOCKET_SEND_BUFFER_SIZE)) { + int sendBufferSize = config.getInt(DefaultDriverOption.SOCKET_SEND_BUFFER_SIZE); + bootstrap = bootstrap.option(ChannelOption.SO_SNDBUF, sendBufferSize); + } + + nettyOptions.afterBootstrapInitialized(bootstrap); + + ChannelFuture connectFuture = bootstrap.connect(endPoint.resolve()); + + connectFuture.addListener( + cf -> { + if (connectFuture.isSuccess()) { + Channel channel = connectFuture.channel(); + DriverChannel driverChannel = + new DriverChannel(endPoint, channel, context.getWriteCoalescer(), currentVersion); + // If this is the first successful connection, remember the protocol version and + // cluster name for future connections. + if (isNegotiating) { + ChannelFactory.this.protocolVersion = currentVersion; + } + if (ChannelFactory.this.clusterName == null) { + ChannelFactory.this.clusterName = driverChannel.getClusterName(); + } + resultFuture.complete(driverChannel); + } else { + Throwable error = connectFuture.cause(); + if (error instanceof UnsupportedProtocolVersionException && isNegotiating) { + attemptedVersions.add(currentVersion); + Optional downgraded = + context.getProtocolVersionRegistry().downgrade(currentVersion); + if (downgraded.isPresent()) { + LOG.info( + "[{}] Failed to connect with protocol {}, retrying with {}", + logPrefix, + currentVersion, + downgraded.get()); + connect( + endPoint, + options, + nodeMetricUpdater, + downgraded.get(), + true, + attemptedVersions, + resultFuture); + } else { + resultFuture.completeExceptionally( + UnsupportedProtocolVersionException.forNegotiation( + endPoint, attemptedVersions)); + } + } else { + // Note: might be completed already if the failure happened in initializer(), this is + // fine + resultFuture.completeExceptionally(error); + } + } + }); + } + + @VisibleForTesting + ChannelInitializer initializer( + EndPoint endPoint, + ProtocolVersion protocolVersion, + DriverChannelOptions options, + NodeMetricUpdater nodeMetricUpdater, + CompletableFuture resultFuture) { + return new ChannelInitializer() { + @Override + protected void initChannel(Channel channel) { + try { + DriverExecutionProfile defaultConfig = context.getConfig().getDefaultProfile(); + + long setKeyspaceTimeoutMillis = + defaultConfig + .getDuration(DefaultDriverOption.CONNECTION_SET_KEYSPACE_TIMEOUT) + .toMillis(); + int maxFrameLength = + (int) defaultConfig.getBytes(DefaultDriverOption.PROTOCOL_MAX_FRAME_LENGTH); + int maxRequestsPerConnection = + defaultConfig.getInt(DefaultDriverOption.CONNECTION_MAX_REQUESTS); + int maxOrphanRequests = + defaultConfig.getInt(DefaultDriverOption.CONNECTION_MAX_ORPHAN_REQUESTS); + + InFlightHandler inFlightHandler = + new InFlightHandler( + protocolVersion, + new StreamIdGenerator(maxRequestsPerConnection), + maxOrphanRequests, + setKeyspaceTimeoutMillis, + channel.newPromise(), + options.eventCallback, + options.ownerLogPrefix); + HeartbeatHandler heartbeatHandler = new HeartbeatHandler(defaultConfig); + ProtocolInitHandler initHandler = + new ProtocolInitHandler( + context, protocolVersion, clusterName, endPoint, options, heartbeatHandler); + + ChannelPipeline pipeline = channel.pipeline(); + context + .getSslHandlerFactory() + .map(f -> f.newSslHandler(channel, endPoint)) + .map(h -> pipeline.addLast("ssl", h)); + + // Only add meter handlers on the pipeline if metrics are enabled. + SessionMetricUpdater sessionMetricUpdater = + context.getMetricsFactory().getSessionUpdater(); + if (nodeMetricUpdater.isEnabled(DefaultNodeMetric.BYTES_RECEIVED, null) + || sessionMetricUpdater.isEnabled(DefaultSessionMetric.BYTES_RECEIVED, null)) { + pipeline.addLast( + "inboundTrafficMeter", + new InboundTrafficMeter(nodeMetricUpdater, sessionMetricUpdater)); + } + + if (nodeMetricUpdater.isEnabled(DefaultNodeMetric.BYTES_SENT, null) + || sessionMetricUpdater.isEnabled(DefaultSessionMetric.BYTES_SENT, null)) { + pipeline.addLast( + "outboundTrafficMeter", + new OutboundTrafficMeter(nodeMetricUpdater, sessionMetricUpdater)); + } + + pipeline + .addLast("encoder", new FrameEncoder(context.getFrameCodec(), maxFrameLength)) + .addLast("decoder", new FrameDecoder(context.getFrameCodec(), maxFrameLength)) + // Note: HeartbeatHandler is inserted here once init completes + .addLast("inflight", inFlightHandler) + .addLast("init", initHandler); + + context.getNettyOptions().afterChannelInitialized(channel); + } catch (Throwable t) { + // If the init handler throws an exception, Netty swallows it and closes the channel. We + // want to propagate it instead, so fail the outer future (the result of connect()). + resultFuture.completeExceptionally(t); + throw t; + } + } + }; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ChannelHandlerRequest.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ChannelHandlerRequest.java new file mode 100644 index 00000000000..0a977b97573 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ChannelHandlerRequest.java @@ -0,0 +1,117 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.DriverTimeoutException; +import com.datastax.oss.driver.internal.core.util.ProtocolUtils; +import com.datastax.oss.driver.internal.core.util.concurrent.UncaughtExceptions; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.response.Error; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import net.jcip.annotations.NotThreadSafe; + +/** Common infrastructure to send a native protocol request from a channel handler. */ +@NotThreadSafe // must be confined to the channel's event loop +abstract class ChannelHandlerRequest implements ResponseCallback { + + final Channel channel; + final ChannelHandlerContext ctx; + private final long timeoutMillis; + + private ScheduledFuture timeoutFuture; + + ChannelHandlerRequest(ChannelHandlerContext ctx, long timeoutMillis) { + this.ctx = ctx; + this.channel = ctx.channel(); + this.timeoutMillis = timeoutMillis; + } + + abstract String describe(); + + abstract Message getRequest(); + + abstract void onResponse(Message response); + + /** either message or cause can be null */ + abstract void fail(String message, Throwable cause); + + void fail(Throwable cause) { + fail(null, cause); + } + + void send() { + assert channel.eventLoop().inEventLoop(); + DriverChannel.RequestMessage message = + new DriverChannel.RequestMessage(getRequest(), false, Frame.NO_PAYLOAD, this); + ChannelFuture writeFuture = channel.writeAndFlush(message); + writeFuture.addListener(this::writeListener); + } + + private void writeListener(Future writeFuture) { + if (writeFuture.isSuccess()) { + timeoutFuture = + channel.eventLoop().schedule(this::onTimeout, timeoutMillis, TimeUnit.MILLISECONDS); + } else { + fail(describe() + ": error writing ", writeFuture.cause()); + } + } + + @Override + public final void onResponse(Frame responseFrame) { + timeoutFuture.cancel(true); + onResponse(responseFrame.message); + } + + @Override + public final void onFailure(Throwable error) { + // timeoutFuture may not have been assigned if write failed. + if (timeoutFuture != null) { + timeoutFuture.cancel(true); + } + fail(describe() + ": unexpected failure", error); + } + + private void onTimeout() { + fail(new DriverTimeoutException(describe() + ": timed out after " + timeoutMillis + " ms")); + if (!channel.closeFuture().isDone()) { + // Cancel the response callback + channel.writeAndFlush(this).addListener(UncaughtExceptions::log); + } + } + + void failOnUnexpected(Message response) { + if (response instanceof Error) { + Error error = (Error) response; + fail( + new IllegalArgumentException( + String.format( + "%s: unexpected server error [%s] %s", + describe(), ProtocolUtils.errorCodeString(error.code), error.message))); + } else { + fail( + new IllegalArgumentException( + String.format( + "%s: unexpected server response opcode=%s", + describe(), ProtocolUtils.opcodeString(response.opcode)))); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ClusterNameMismatchException.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ClusterNameMismatchException.java new file mode 100644 index 00000000000..04abdfb0368 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ClusterNameMismatchException.java @@ -0,0 +1,57 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; + +/** + * Indicates that we've attempted to connect to a node with a cluster name that doesn't match that + * of the other nodes known to the driver. + * + *

The driver runs the following query on each newly established connection: + * + *

+ *     select cluster_name from system.local
+ * 
+ * + * The first connection sets the cluster name for this driver instance, all subsequent connections + * must match it or they will get rejected. This is intended to filter out errors in the discovery + * process (for example, stale entries in {@code system.peers}). + * + *

This error is never returned directly to the client. If we detect a mismatch, it will always + * be after the driver has connected successfully; the error will be logged and the offending node + * forced down. + */ +public class ClusterNameMismatchException extends RuntimeException { + + private static final long serialVersionUID = 0; + + public final EndPoint endPoint; + public final String expectedClusterName; + public final String actualClusterName; + + public ClusterNameMismatchException( + EndPoint endPoint, String actualClusterName, String expectedClusterName) { + super( + String.format( + "Node %s reports cluster name '%s' that doesn't match our cluster name '%s'. " + + "It will be forced down.", + endPoint, actualClusterName, expectedClusterName)); + this.endPoint = endPoint; + this.expectedClusterName = expectedClusterName; + this.actualClusterName = actualClusterName; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ConnectInitHandler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ConnectInitHandler.java new file mode 100644 index 00000000000..4c7b7f642fe --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ConnectInitHandler.java @@ -0,0 +1,81 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.PromiseCombiner; +import java.net.SocketAddress; +import net.jcip.annotations.NotThreadSafe; + +/** + * A handler that delays the promise returned by {@code bootstrap.connect()}, in order to run a + * custom initialization process before making the channel available to clients. + * + *

This handler is not shareable. It must be installed by the channel initializer, as the last + * channel in the pipeline. + * + *

It will be notified via {@link #onRealConnect(ChannelHandlerContext)} when the real underlying + * connection is established. It can then start sending messages on the connection, while external + * clients are still waiting on their promise. Once the custom initialization is finished, the + * clients' promise can be completed with {@link #setConnectSuccess()} or {@link + * #setConnectFailure(Throwable)}. + */ +@NotThreadSafe +public abstract class ConnectInitHandler extends ChannelDuplexHandler { + // the completion of the custom initialization process + private ChannelPromise initPromise; + private ChannelHandlerContext ctx; + + @Override + public void connect( + ChannelHandlerContext ctx, + SocketAddress remoteAddress, + SocketAddress localAddress, + ChannelPromise callerPromise) + throws Exception { + this.ctx = ctx; + initPromise = ctx.channel().newPromise(); + + // the completion of the real underlying connection: + ChannelPromise realConnectPromise = ctx.channel().newPromise(); + super.connect(ctx, remoteAddress, localAddress, realConnectPromise); + realConnectPromise.addListener(future -> onRealConnect(ctx)); + + // Make the caller's promise wait on the other two: + PromiseCombiner combiner = new PromiseCombiner(); + combiner.addAll(new Future[] {realConnectPromise, initPromise}); + combiner.finish(callerPromise); + } + + protected abstract void onRealConnect(ChannelHandlerContext ctx); + + protected boolean setConnectSuccess() { + boolean result = initPromise.trySuccess(); + if (result) { + ctx.pipeline().remove(this); + } + return result; + } + + protected void setConnectFailure(Throwable cause) { + if (initPromise.tryFailure(cause)) { + ctx.channel().close(); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DefaultWriteCoalescer.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DefaultWriteCoalescer.java new file mode 100644 index 00000000000..29bf2822617 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DefaultWriteCoalescer.java @@ -0,0 +1,147 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoop; +import java.util.HashSet; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import net.jcip.annotations.ThreadSafe; + +/** + * Default write coalescing strategy. + * + *

It maintains a queue per event loop, with the writes targeting the channels that run on this + * loop. As soon as a write gets enqueued, it triggers a task that will flush the queue (other + * writes can get enqueued before the task runs). Once that task is complete, it re-triggers itself + * as long as new writes have been enqueued, or {@code maxRunsWithNoWork} times if there are no more + * tasks. + * + *

Note that Netty provides a similar mechanism out of the box ({@link + * io.netty.handler.flush.FlushConsolidationHandler}), but in our experience our approach allows + * more performance gains, because it allows consolidating not only the flushes, but also the write + * tasks themselves (a single consolidated write task is scheduled on the event loop, instead of + * multiple individual tasks, so there is less context switching). + */ +@ThreadSafe +public class DefaultWriteCoalescer implements WriteCoalescer { + private final int maxRunsWithNoWork; + private final long rescheduleIntervalNanos; + private final ConcurrentMap flushers = new ConcurrentHashMap<>(); + + public DefaultWriteCoalescer(DriverContext context) { + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + maxRunsWithNoWork = config.getInt(DefaultDriverOption.COALESCER_MAX_RUNS); + rescheduleIntervalNanos = config.getDuration(DefaultDriverOption.COALESCER_INTERVAL).toNanos(); + } + + @Override + public ChannelFuture writeAndFlush(Channel channel, Object message) { + ChannelPromise writePromise = channel.newPromise(); + Write write = new Write(channel, message, writePromise); + enqueue(write, channel.eventLoop()); + return writePromise; + } + + private void enqueue(Write write, EventLoop eventLoop) { + Flusher flusher = flushers.computeIfAbsent(eventLoop, Flusher::new); + flusher.enqueue(write); + } + + private class Flusher { + private final EventLoop eventLoop; + + // These variables are accessed both from client threads and the event loop + private final Queue writes = new ConcurrentLinkedQueue<>(); + private final AtomicBoolean running = new AtomicBoolean(); + + // These variables are accessed only from runOnEventLoop, they don't need to be thread-safe + private final Set channels = new HashSet<>(); + private int runsWithNoWork = 0; + + private Flusher(EventLoop eventLoop) { + this.eventLoop = eventLoop; + } + + private void enqueue(Write write) { + boolean added = writes.offer(write); + assert added; // always true (see MpscLinkedAtomicQueue implementation) + if (running.compareAndSet(false, true)) { + eventLoop.execute(this::runOnEventLoop); + } + } + + private void runOnEventLoop() { + assert eventLoop.inEventLoop(); + + boolean didSomeWork = false; + Write write; + while ((write = writes.poll()) != null) { + Channel channel = write.channel; + channels.add(channel); + channel.write(write.message, write.writePromise); + didSomeWork = true; + } + + for (Channel channel : channels) { + channel.flush(); + } + channels.clear(); + + if (didSomeWork) { + runsWithNoWork = 0; + } else if (++runsWithNoWork > maxRunsWithNoWork) { + // Prepare to stop + running.set(false); + // If no new writes have been enqueued since the previous line, we can return safely + if (writes.isEmpty()) { + return; + } + // Otherwise check if those writes have triggered a new run. If not, we need to do that + // ourselves (i.e. not return yet) + if (!running.compareAndSet(false, true)) { + return; + } + } + if (!eventLoop.isShuttingDown()) { + eventLoop.schedule(this::runOnEventLoop, rescheduleIntervalNanos, TimeUnit.NANOSECONDS); + } + } + } + + private static class Write { + private final Channel channel; + private final Object message; + private final ChannelPromise writePromise; + + private Write(Channel channel, Object message, ChannelPromise writePromise) { + this.channel = channel; + this.message = message; + this.writePromise = writePromise; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannel.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannel.java new file mode 100644 index 00000000000..59978777b98 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannel.java @@ -0,0 +1,256 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.internal.core.util.concurrent.UncaughtExceptions; +import com.datastax.oss.protocol.internal.Message; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoop; +import io.netty.util.AttributeKey; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import net.jcip.annotations.ThreadSafe; + +/** + * A thin wrapper around a Netty {@link Channel}, to send requests to a Cassandra node and receive + * responses. + */ +@ThreadSafe +public class DriverChannel { + static final AttributeKey CLUSTER_NAME_KEY = AttributeKey.newInstance("cluster_name"); + + @SuppressWarnings("RedundantStringConstructorCall") + static final Object GRACEFUL_CLOSE_MESSAGE = new String("GRACEFUL_CLOSE_MESSAGE"); + + @SuppressWarnings("RedundantStringConstructorCall") + static final Object FORCEFUL_CLOSE_MESSAGE = new String("FORCEFUL_CLOSE_MESSAGE"); + + private final EndPoint endPoint; + private final Channel channel; + private final InFlightHandler inFlightHandler; + private final WriteCoalescer writeCoalescer; + private final ProtocolVersion protocolVersion; + private final AtomicBoolean closing = new AtomicBoolean(); + private final AtomicBoolean forceClosing = new AtomicBoolean(); + + DriverChannel( + EndPoint endPoint, + Channel channel, + WriteCoalescer writeCoalescer, + ProtocolVersion protocolVersion) { + this.endPoint = endPoint; + this.channel = channel; + this.inFlightHandler = channel.pipeline().get(InFlightHandler.class); + this.writeCoalescer = writeCoalescer; + this.protocolVersion = protocolVersion; + } + + /** + * @return a future that succeeds when the request frame was successfully written on the channel. + * Beyond that, the caller will be notified through the {@code responseCallback}. + */ + public Future write( + Message request, + boolean tracing, + Map customPayload, + ResponseCallback responseCallback) { + if (closing.get()) { + return channel.newFailedFuture(new IllegalStateException("Driver channel is closing")); + } + RequestMessage message = new RequestMessage(request, tracing, customPayload, responseCallback); + return writeCoalescer.writeAndFlush(channel, message); + } + + /** + * Cancels a callback, indicating that the client that wrote it is no longer interested in the + * answer. + * + *

Note that this does not cancel the request server-side (but might in the future if Cassandra + * supports it). + */ + public void cancel(ResponseCallback responseCallback) { + // To avoid creating an extra message, we adopt the convention that writing the callback + // directly means cancellation + writeCoalescer.writeAndFlush(channel, responseCallback).addListener(UncaughtExceptions::log); + } + + /** + * Switches the underlying Cassandra connection to a new keyspace (as if a {@code USE ...} + * statement was issued). + * + *

The future will complete once the change is effective. Only one change may run at a given + * time, concurrent attempts will fail. + * + *

Changing the keyspace is inherently thread-unsafe: if other queries are running at the same + * time, the keyspace they will use is unpredictable. + */ + public Future setKeyspace(CqlIdentifier newKeyspace) { + Promise promise = channel.eventLoop().newPromise(); + channel.pipeline().fireUserEventTriggered(new SetKeyspaceEvent(newKeyspace, promise)); + return promise; + } + + /** + * @return the name of the Cassandra cluster as returned by {@code system.local.cluster_name} on + * this connection. + */ + public String getClusterName() { + return channel.attr(CLUSTER_NAME_KEY).get(); + } + + /** + * @return the number of available stream ids on the channel. This is used to weigh channels in + * pools that have a size bigger than 1, in the load balancing policy, and for monitoring + * purposes. + */ + public int getAvailableIds() { + return inFlightHandler.getAvailableIds(); + } + + /** + * @return the number of requests currently executing on this channel (including {@link + * #getOrphanedIds() orphaned ids}). + */ + public int getInFlight() { + return inFlightHandler.getInFlight(); + } + + /** + * @return the number of stream ids for requests that have either timed out or been cancelled, but + * for which we can't release the stream id because a request might still come from the + * server. + */ + public int getOrphanedIds() { + return inFlightHandler.getOrphanIds(); + } + + public EventLoop eventLoop() { + return channel.eventLoop(); + } + + public ProtocolVersion protocolVersion() { + return protocolVersion; + } + + /** The endpoint that was used to establish the connection. */ + public EndPoint getEndPoint() { + return endPoint; + } + + public SocketAddress localAddress() { + return channel.localAddress(); + } + + /** @return The {@link ChannelConfig configuration} of this channel. */ + public ChannelConfig config() { + return channel.config(); + } + + /** + * Initiates a graceful shutdown: no new requests will be accepted, but all pending requests will + * be allowed to complete before the underlying channel is closed. + */ + public Future close() { + if (closing.compareAndSet(false, true) && channel.isOpen()) { + // go through the coalescer: this guarantees that we won't reject writes that were submitted + // before, but had not been coalesced yet. + writeCoalescer + .writeAndFlush(channel, GRACEFUL_CLOSE_MESSAGE) + .addListener(UncaughtExceptions::log); + } + return channel.closeFuture(); + } + + /** + * Initiates a forced shutdown: any pending request will be aborted and the underlying channel + * will be closed. + */ + public Future forceClose() { + this.close(); + if (forceClosing.compareAndSet(false, true) && channel.isOpen()) { + writeCoalescer + .writeAndFlush(channel, FORCEFUL_CLOSE_MESSAGE) + .addListener(UncaughtExceptions::log); + } + return channel.closeFuture(); + } + + /** + * Returns a future that will complete when a graceful close has started, but not yet completed. + * + *

In other words, the channel has stopped accepting new requests, but is still waiting for + * pending requests to finish. Once the last response has been received, the channel will really + * close and {@link #closeFuture()} will be completed. + * + *

If there were no pending requests when the graceful shutdown was initiated, or if {@link + * #forceClose()} is called first, this future will never complete. + */ + public ChannelFuture closeStartedFuture() { + return this.inFlightHandler.closeStartedFuture; + } + + /** + * Does not close the channel, but returns a future that will complete when it is completely + * closed. + */ + public ChannelFuture closeFuture() { + return channel.closeFuture(); + } + + @Override + public String toString() { + return channel.toString(); + } + + // This is essentially a stripped-down Frame. We can't materialize the frame before writing, + // because we need the stream id, which is assigned from within the event loop. + static class RequestMessage { + final Message request; + final boolean tracing; + final Map customPayload; + final ResponseCallback responseCallback; + + RequestMessage( + Message message, + boolean tracing, + Map customPayload, + ResponseCallback responseCallback) { + this.request = message; + this.tracing = tracing; + this.customPayload = customPayload; + this.responseCallback = responseCallback; + } + } + + static class SetKeyspaceEvent { + final CqlIdentifier keyspaceName; + final Promise promise; + + public SetKeyspaceEvent(CqlIdentifier keyspaceName, Promise promise) { + this.keyspaceName = keyspaceName; + this.promise = promise; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannelOptions.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannelOptions.java new file mode 100644 index 00000000000..258f1ab0c42 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/DriverChannelOptions.java @@ -0,0 +1,87 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import java.util.Collections; +import java.util.List; +import net.jcip.annotations.Immutable; + +/** Options for the creation of a driver channel. */ +@Immutable +public class DriverChannelOptions { + + /** No keyspace, no events, don't report available stream ids. */ + public static DriverChannelOptions DEFAULT = builder().build(); + + public static Builder builder() { + return new Builder(); + } + + public final CqlIdentifier keyspace; + + /** + * What kind of protocol events to listen for. + * + * @see com.datastax.oss.protocol.internal.ProtocolConstants.EventType + */ + public final List eventTypes; + + public final EventCallback eventCallback; + + public final String ownerLogPrefix; + + private DriverChannelOptions( + CqlIdentifier keyspace, + List eventTypes, + EventCallback eventCallback, + String ownerLogPrefix) { + this.keyspace = keyspace; + this.eventTypes = eventTypes; + this.eventCallback = eventCallback; + this.ownerLogPrefix = ownerLogPrefix; + } + + public static class Builder { + private CqlIdentifier keyspace = null; + private List eventTypes = Collections.emptyList(); + private EventCallback eventCallback = null; + private String ownerLogPrefix = null; + + public Builder withKeyspace(CqlIdentifier keyspace) { + this.keyspace = keyspace; + return this; + } + + public Builder withEvents(List eventTypes, EventCallback eventCallback) { + Preconditions.checkArgument(eventTypes != null && !eventTypes.isEmpty()); + Preconditions.checkNotNull(eventCallback); + this.eventTypes = eventTypes; + this.eventCallback = eventCallback; + return this; + } + + public Builder withOwnerLogPrefix(String ownerLogPrefix) { + this.ownerLogPrefix = ownerLogPrefix; + return this; + } + + public DriverChannelOptions build() { + return new DriverChannelOptions(keyspace, eventTypes, eventCallback, ownerLogPrefix); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/EventCallback.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/EventCallback.java new file mode 100644 index 00000000000..df85d8ca7b0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/EventCallback.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.protocol.internal.Message; + +public interface EventCallback { + /** Invoked when a protocol event is received. */ + void onEvent(Message event); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/HeartbeatHandler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/HeartbeatHandler.java new file mode 100644 index 00000000000..0c4dba9ffc4 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/HeartbeatHandler.java @@ -0,0 +1,112 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.HeartbeatException; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.request.Options; +import com.datastax.oss.protocol.internal.response.Supported; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +import io.netty.handler.timeout.IdleStateHandler; +import net.jcip.annotations.NotThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@NotThreadSafe +class HeartbeatHandler extends IdleStateHandler { + + private static final Logger LOG = LoggerFactory.getLogger(HeartbeatHandler.class); + + private final DriverExecutionProfile config; + + private HeartbeatRequest request; + + HeartbeatHandler(DriverExecutionProfile config) { + super((int) config.getDuration(DefaultDriverOption.HEARTBEAT_INTERVAL).getSeconds(), 0, 0); + this.config = config; + } + + @Override + protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception { + if (evt.state() == IdleState.READER_IDLE) { + if (this.request != null) { + LOG.warn( + "Not sending heartbeat because a previous one is still in progress. " + + "Check that {} is not lower than {}.", + DefaultDriverOption.HEARTBEAT_INTERVAL.getPath(), + DefaultDriverOption.HEARTBEAT_TIMEOUT.getPath()); + } else { + LOG.debug( + "Connection was inactive for {} seconds, sending heartbeat", + config.getDuration(DefaultDriverOption.HEARTBEAT_INTERVAL).getSeconds()); + long timeoutMillis = config.getDuration(DefaultDriverOption.HEARTBEAT_TIMEOUT).toMillis(); + this.request = new HeartbeatRequest(ctx, timeoutMillis); + this.request.send(); + } + } + } + + private class HeartbeatRequest extends ChannelHandlerRequest { + + HeartbeatRequest(ChannelHandlerContext ctx, long timeoutMillis) { + super(ctx, timeoutMillis); + } + + @Override + String describe() { + return "heartbeat"; + } + + @Override + Message getRequest() { + return Options.INSTANCE; + } + + @Override + void onResponse(Message response) { + if (response instanceof Supported) { + LOG.debug("{} Heartbeat query succeeded", ctx.channel()); + HeartbeatHandler.this.request = null; + } else { + failOnUnexpected(response); + } + } + + @Override + void fail(String message, Throwable cause) { + if (cause instanceof HeartbeatException) { + // Ignore: this happens when the heartbeat query times out and the inflight handler aborts + // all queries (including the heartbeat query itself) + return; + } + + HeartbeatHandler.this.request = null; + if (message != null) { + LOG.debug("{} Heartbeat query failed: {}", ctx.channel(), message, cause); + } else { + LOG.debug("{} Heartbeat query failed", ctx.channel(), cause); + } + + // Notify InFlightHandler. + ctx.fireExceptionCaught( + new HeartbeatException(ctx.channel().remoteAddress(), message, cause)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/InFlightHandler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/InFlightHandler.java new file mode 100644 index 00000000000..d6d69306871 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/InFlightHandler.java @@ -0,0 +1,435 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.connection.BusyConnectionException; +import com.datastax.oss.driver.api.core.connection.ClosedConnectionException; +import com.datastax.oss.driver.api.core.connection.HeartbeatException; +import com.datastax.oss.driver.internal.core.channel.DriverChannel.RequestMessage; +import com.datastax.oss.driver.internal.core.channel.DriverChannel.SetKeyspaceEvent; +import com.datastax.oss.driver.internal.core.protocol.FrameDecodingException; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.shaded.guava.common.collect.BiMap; +import com.datastax.oss.driver.shaded.guava.common.collect.HashBiMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.request.Query; +import com.datastax.oss.protocol.internal.response.result.SetKeyspace; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.util.concurrent.Promise; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import net.jcip.annotations.NotThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Manages requests that are currently executing on a channel. */ +@NotThreadSafe +public class InFlightHandler extends ChannelDuplexHandler { + private static final Logger LOG = LoggerFactory.getLogger(InFlightHandler.class); + + private final ProtocolVersion protocolVersion; + private final StreamIdGenerator streamIds; + final ChannelPromise closeStartedFuture; + private final String ownerLogPrefix; + private final BiMap inFlight; + private final Map orphaned; + private volatile int orphanedSize; // thread-safe view for metrics + private final long setKeyspaceTimeoutMillis; + private final EventCallback eventCallback; + private final int maxOrphanStreamIds; + private boolean closingGracefully; + private SetKeyspaceRequest setKeyspaceRequest; + private String logPrefix; + + InFlightHandler( + ProtocolVersion protocolVersion, + StreamIdGenerator streamIds, + int maxOrphanStreamIds, + long setKeyspaceTimeoutMillis, + ChannelPromise closeStartedFuture, + EventCallback eventCallback, + String ownerLogPrefix) { + this.protocolVersion = protocolVersion; + this.streamIds = streamIds; + this.maxOrphanStreamIds = maxOrphanStreamIds; + this.closeStartedFuture = closeStartedFuture; + this.ownerLogPrefix = ownerLogPrefix; + this.logPrefix = ownerLogPrefix + "|connecting..."; + this.inFlight = HashBiMap.create(streamIds.getMaxAvailableIds()); + this.orphaned = new HashMap<>(maxOrphanStreamIds); + this.setKeyspaceTimeoutMillis = setKeyspaceTimeoutMillis; + this.eventCallback = eventCallback; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + String channelId = ctx.channel().toString(); + this.logPrefix = ownerLogPrefix + "|" + channelId.substring(1, channelId.length() - 1); + } + + @Override + public void write(ChannelHandlerContext ctx, Object in, ChannelPromise promise) throws Exception { + if (in == DriverChannel.GRACEFUL_CLOSE_MESSAGE) { + LOG.debug("[{}] Received graceful close request", logPrefix); + startGracefulShutdown(ctx); + } else if (in == DriverChannel.FORCEFUL_CLOSE_MESSAGE) { + LOG.debug("[{}] Received forceful close request, aborting pending queries", logPrefix); + abortAllInFlight(new ClosedConnectionException("Channel was force-closed")); + ctx.channel().close(); + } else if (in instanceof HeartbeatException) { + abortAllInFlight( + new ClosedConnectionException("Heartbeat query failed", ((HeartbeatException) in))); + ctx.close(); + } else if (in instanceof RequestMessage) { + write(ctx, (RequestMessage) in, promise); + } else if (in instanceof ResponseCallback) { + cancel(ctx, (ResponseCallback) in, promise); + } else { + promise.setFailure( + new IllegalArgumentException("Unsupported message type " + in.getClass().getName())); + } + } + + private void write(ChannelHandlerContext ctx, RequestMessage message, ChannelPromise promise) { + if (closingGracefully) { + promise.setFailure(new IllegalStateException("Channel is closing")); + return; + } + int streamId = streamIds.acquire(); + if (streamId < 0) { + promise.setFailure(new BusyConnectionException(streamIds.getMaxAvailableIds())); + return; + } + + if (inFlight.containsKey(streamId)) { + promise.setFailure( + new IllegalStateException("Found pending callback for stream id " + streamId)); + return; + } + + LOG.trace("[{}] Writing {} on stream id {}", logPrefix, message.responseCallback, streamId); + Frame frame = + Frame.forRequest( + protocolVersion.getCode(), + streamId, + message.tracing, + message.customPayload, + message.request); + + inFlight.put(streamId, message.responseCallback); + ChannelFuture writeFuture = ctx.write(frame, promise); + writeFuture.addListener( + future -> { + if (future.isSuccess()) { + message.responseCallback.onStreamIdAssigned(streamId); + } else { + release(streamId, ctx); + } + }); + } + + private void cancel( + ChannelHandlerContext ctx, ResponseCallback responseCallback, ChannelPromise promise) { + Integer streamId = inFlight.inverse().remove(responseCallback); + if (streamId == null) { + LOG.trace( + "[{}] Received cancellation for unknown or already cancelled callback {}, skipping", + logPrefix, + responseCallback); + } else { + LOG.trace( + "[{}] Cancelled callback {} for stream id {}", logPrefix, responseCallback, streamId); + if (closingGracefully && inFlight.isEmpty()) { + LOG.debug("[{}] Last pending query was cancelled, closing channel", logPrefix); + ctx.channel().close(); + } else { + // We can't release the stream id, because a response might still come back from the server. + // Keep track of those "orphaned" ids, to release them later if we get a response and the + // callback says it's the last one. + orphaned.put(streamId, responseCallback); + if (orphaned.size() > maxOrphanStreamIds) { + LOG.debug( + "[{}] Orphan stream ids exceeded the configured threshold ({}), closing gracefully", + logPrefix, + maxOrphanStreamIds); + startGracefulShutdown(ctx); + } else { + orphanedSize = orphaned.size(); + } + } + } + promise.setSuccess(); + } + + private void startGracefulShutdown(ChannelHandlerContext ctx) { + if (inFlight.isEmpty()) { + LOG.debug("[{}] No pending queries, completing graceful shutdown now", logPrefix); + ctx.channel().close(); + } else { + // remove heartbeat handler from pipeline if present. + ChannelHandler heartbeatHandler = ctx.pipeline().get("heartbeat"); + if (heartbeatHandler != null) { + ctx.pipeline().remove(heartbeatHandler); + } + LOG.debug("[{}] There are pending queries, delaying graceful shutdown", logPrefix); + closingGracefully = true; + closeStartedFuture.setSuccess(); + } + } + + @Override + @SuppressWarnings("NonAtomicVolatileUpdate") + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Frame responseFrame = (Frame) msg; + int streamId = responseFrame.streamId; + + if (streamId < 0) { + Message event = responseFrame.message; + if (eventCallback == null) { + LOG.debug("[{}] Received event {} but no callback was registered", logPrefix, event); + } else { + LOG.debug("[{}] Received event {}, notifying callback", logPrefix, event); + try { + eventCallback.onEvent(event); + } catch (Throwable t) { + Loggers.warnWithException( + LOG, "[{}] Unexpected error while invoking event handler", logPrefix, t); + } + } + } else { + boolean wasInFlight = true; + ResponseCallback callback = inFlight.get(streamId); + if (callback == null) { + wasInFlight = false; + callback = orphaned.get(streamId); + if (callback == null) { + LOG.trace("[{}] Got response on unknown stream id {}, skipping", streamId); + return; + } + } + try { + if (callback.isLastResponse(responseFrame)) { + LOG.debug( + "[{}] Got last response on {} stream id {}, completing and releasing", + logPrefix, + wasInFlight ? "in-flight" : "orphaned", + streamId); + release(streamId, ctx); + } else { + LOG.trace( + "[{}] Got non-last response on {} stream id {}, still holding", + logPrefix, + wasInFlight ? "in-flight" : "orphaned", + streamId); + } + if (wasInFlight) { + callback.onResponse(responseFrame); + } + } catch (Throwable t) { + if (wasInFlight) { + callback.onFailure( + new IllegalArgumentException("Unexpected error while invoking response handler", t)); + } else { + // Assume the callback is already completed, so it's better to log + Loggers.warnWithException( + LOG, + "[{}] Unexpected error while invoking response handler on stream id {}", + logPrefix, + t, + streamId); + } + } + } + } + + /** Called if an exception was thrown while processing an inbound event (i.e. a response). */ + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable exception) throws Exception { + if (exception instanceof FrameDecodingException) { + int streamId = ((FrameDecodingException) exception).streamId; + LOG.debug("[{}] Error while decoding response on stream id {}", logPrefix, streamId); + if (streamId >= 0) { + // We know which request matches the failing response, fail that one only + ResponseCallback responseCallback = inFlight.get(streamId); + if (responseCallback != null) { + try { + responseCallback.onFailure(exception.getCause()); + } catch (Throwable t) { + Loggers.warnWithException( + LOG, "[{}] Unexpected error while invoking failure handler", logPrefix, t); + } + } + release(streamId, ctx); + } else { + Loggers.warnWithException( + LOG, + "[{}] Unexpected error while decoding incoming event frame", + logPrefix, + exception.getCause()); + } + } else { + // Otherwise fail all pending requests + abortAllInFlight( + (exception instanceof HeartbeatException) + ? (HeartbeatException) exception + : new ClosedConnectionException("Unexpected error on channel", exception)); + ctx.close(); + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object event) throws Exception { + if (event instanceof SetKeyspaceEvent) { + SetKeyspaceEvent setKeyspaceEvent = (SetKeyspaceEvent) event; + if (this.setKeyspaceRequest != null) { + setKeyspaceEvent.promise.setFailure( + new IllegalStateException( + "Can't call setKeyspace while a keyspace switch is already in progress")); + } else { + LOG.debug( + "[{}] Switching to keyspace {}", logPrefix, setKeyspaceEvent.keyspaceName.asInternal()); + this.setKeyspaceRequest = new SetKeyspaceRequest(ctx, setKeyspaceEvent); + this.setKeyspaceRequest.send(); + } + } else { + super.userEventTriggered(ctx, event); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + // If the channel was closed normally (normal or forced shutdown), inFlight is already empty by + // the time we get here. So if it's not, it means the channel closed unexpectedly (e.g. the + // connection was dropped). + abortAllInFlight(new ClosedConnectionException("Lost connection to remote peer")); + super.channelInactive(ctx); + } + + private void release(int streamId, ChannelHandlerContext ctx) { + LOG.trace("[{}] Releasing stream id {}", logPrefix, streamId); + if (inFlight.remove(streamId) != null) { + // If we're in the middle of an orderly close and this was the last request, actually close + // the channel now + if (closingGracefully && inFlight.isEmpty()) { + LOG.debug("[{}] Done handling the last pending query, closing channel", logPrefix); + ctx.channel().close(); + } + } else if (orphaned.remove(streamId) != null) { + orphanedSize = orphaned.size(); + } + // Note: it's possible that the callback is in neither map, if we get here after a call to + // abortAllInFlight that already cleared the map (see JAVA-2000) + streamIds.release(streamId); + } + + private void abortAllInFlight(DriverException cause) { + abortAllInFlight(cause, null); + } + + /** + * @param ignore the ResponseCallback that called this method, if applicable (avoids a recursive + * loop) + */ + private void abortAllInFlight(DriverException cause, ResponseCallback ignore) { + if (!inFlight.isEmpty()) { + // Clear the map now and iterate on a copy, in case one of the onFailure calls below recurses + // back into this method + Set toAbort = ImmutableSet.copyOf(inFlight.values()); + inFlight.clear(); + for (ResponseCallback responseCallback : toAbort) { + if (responseCallback != ignore) { + responseCallback.onFailure(cause); + } + } + // It's not necessary to release the stream ids, since we always call this method right before + // closing the channel + } + } + + int getAvailableIds() { + return streamIds.getAvailableIds(); + } + + int getInFlight() { + return streamIds.getMaxAvailableIds() - streamIds.getAvailableIds(); + } + + int getOrphanIds() { + return orphanedSize; + } + + private class SetKeyspaceRequest extends ChannelHandlerRequest { + + private final CqlIdentifier keyspaceName; + private final Promise promise; + + SetKeyspaceRequest(ChannelHandlerContext ctx, SetKeyspaceEvent setKeyspaceEvent) { + super(ctx, setKeyspaceTimeoutMillis); + this.keyspaceName = setKeyspaceEvent.keyspaceName; + this.promise = setKeyspaceEvent.promise; + } + + @Override + String describe() { + return "[" + logPrefix + "] set keyspace " + keyspaceName; + } + + @Override + Message getRequest() { + return new Query("USE " + keyspaceName.asCql(false)); + } + + @Override + void onResponse(Message response) { + if (response instanceof SetKeyspace) { + if (promise.trySuccess(null)) { + InFlightHandler.this.setKeyspaceRequest = null; + } + } else { + failOnUnexpected(response); + } + } + + @Override + void fail(String message, Throwable cause) { + ClosedConnectionException setKeyspaceException = + new ClosedConnectionException(message, cause); + if (promise.tryFailure(setKeyspaceException)) { + InFlightHandler.this.setKeyspaceRequest = null; + // setKeyspace queries are not triggered directly by the user, but only as a response to a + // successful "USE... query", so the keyspace name should generally be valid. If the + // keyspace switch fails, this could be due to a schema disagreement or a more serious + // error. Rescheduling the switch is impractical, we can't do much better than closing the + // channel and letting it reconnect. + Loggers.warnWithException( + LOG, "[{}] Unexpected error while switching keyspace", logPrefix, setKeyspaceException); + abortAllInFlight(setKeyspaceException, this); + ctx.channel().close(); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/InboundTrafficMeter.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/InboundTrafficMeter.java new file mode 100644 index 00000000000..eea4b8a6179 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/InboundTrafficMeter.java @@ -0,0 +1,46 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; +import com.datastax.oss.driver.internal.core.metrics.NodeMetricUpdater; +import com.datastax.oss.driver.internal.core.metrics.SessionMetricUpdater; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; + +public class InboundTrafficMeter extends ChannelInboundHandlerAdapter { + + private final NodeMetricUpdater nodeMetricUpdater; + private final SessionMetricUpdater sessionMetricUpdater; + + InboundTrafficMeter( + NodeMetricUpdater nodeMetricUpdater, SessionMetricUpdater sessionMetricUpdater) { + this.nodeMetricUpdater = nodeMetricUpdater; + this.sessionMetricUpdater = sessionMetricUpdater; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof ByteBuf) { + int bytes = ((ByteBuf) msg).readableBytes(); + nodeMetricUpdater.markMeter(DefaultNodeMetric.BYTES_RECEIVED, null, bytes); + sessionMetricUpdater.markMeter(DefaultSessionMetric.BYTES_RECEIVED, null, bytes); + } + super.channelRead(ctx, msg); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/OutboundTrafficMeter.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/OutboundTrafficMeter.java new file mode 100644 index 00000000000..e07bcac99ed --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/OutboundTrafficMeter.java @@ -0,0 +1,48 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; +import com.datastax.oss.driver.internal.core.metrics.NodeMetricUpdater; +import com.datastax.oss.driver.internal.core.metrics.SessionMetricUpdater; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; + +public class OutboundTrafficMeter extends ChannelOutboundHandlerAdapter { + + private final NodeMetricUpdater nodeMetricUpdater; + private final SessionMetricUpdater sessionMetricUpdater; + + OutboundTrafficMeter( + NodeMetricUpdater nodeMetricUpdater, SessionMetricUpdater sessionMetricUpdater) { + this.nodeMetricUpdater = nodeMetricUpdater; + this.sessionMetricUpdater = sessionMetricUpdater; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) + throws Exception { + if (msg instanceof ByteBuf) { + int bytes = ((ByteBuf) msg).readableBytes(); + nodeMetricUpdater.markMeter(DefaultNodeMetric.BYTES_SENT, null, bytes); + sessionMetricUpdater.markMeter(DefaultSessionMetric.BYTES_SENT, null, bytes); + } + super.write(ctx, msg, promise); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/PassThroughWriteCoalescer.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/PassThroughWriteCoalescer.java new file mode 100644 index 00000000000..d8870bc3813 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/PassThroughWriteCoalescer.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import net.jcip.annotations.ThreadSafe; + +/** No-op implementation of the write coalescer: each write is flushed immediately. */ +@ThreadSafe +public class PassThroughWriteCoalescer implements WriteCoalescer { + + public PassThroughWriteCoalescer(@SuppressWarnings("unused") DriverContext context) { + // nothing to do + } + + @Override + public ChannelFuture writeAndFlush(Channel channel, Object message) { + return channel.writeAndFlush(message); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java new file mode 100644 index 00000000000..54cb427e365 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandler.java @@ -0,0 +1,323 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.InvalidKeyspaceException; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException; +import com.datastax.oss.driver.api.core.auth.AuthenticationException; +import com.datastax.oss.driver.api.core.auth.Authenticator; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.ConnectionInitException; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.util.ProtocolUtils; +import com.datastax.oss.driver.internal.core.util.concurrent.UncaughtExceptions; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.request.AuthResponse; +import com.datastax.oss.protocol.internal.request.Query; +import com.datastax.oss.protocol.internal.request.Register; +import com.datastax.oss.protocol.internal.request.Startup; +import com.datastax.oss.protocol.internal.response.AuthChallenge; +import com.datastax.oss.protocol.internal.response.AuthSuccess; +import com.datastax.oss.protocol.internal.response.Authenticate; +import com.datastax.oss.protocol.internal.response.Error; +import com.datastax.oss.protocol.internal.response.Ready; +import com.datastax.oss.protocol.internal.response.result.Rows; +import com.datastax.oss.protocol.internal.response.result.SetKeyspace; +import io.netty.channel.ChannelHandlerContext; +import java.nio.ByteBuffer; +import java.util.List; +import net.jcip.annotations.NotThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handles the sequence of internal requests that we send on a channel before it's ready to accept + * user requests. + */ +@NotThreadSafe +class ProtocolInitHandler extends ConnectInitHandler { + private static final Logger LOG = LoggerFactory.getLogger(ProtocolInitHandler.class); + private static final Query CLUSTER_NAME_QUERY = + new Query("SELECT cluster_name FROM system.local"); + + private final InternalDriverContext context; + private final long timeoutMillis; + private final ProtocolVersion initialProtocolVersion; + private final DriverChannelOptions options; + // might be null if this is the first channel to this cluster + private final String expectedClusterName; + private final EndPoint endPoint; + private final HeartbeatHandler heartbeatHandler; + private String logPrefix; + private ChannelHandlerContext ctx; + + ProtocolInitHandler( + InternalDriverContext context, + ProtocolVersion protocolVersion, + String expectedClusterName, + EndPoint endPoint, + DriverChannelOptions options, + HeartbeatHandler heartbeatHandler) { + + this.context = context; + this.endPoint = endPoint; + + DriverExecutionProfile defaultConfig = context.getConfig().getDefaultProfile(); + + this.timeoutMillis = + defaultConfig.getDuration(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT).toMillis(); + this.initialProtocolVersion = protocolVersion; + this.expectedClusterName = expectedClusterName; + this.options = options; + this.heartbeatHandler = heartbeatHandler; + this.logPrefix = options.ownerLogPrefix + "|connecting..."; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + String channelId = ctx.channel().toString(); + this.logPrefix = options.ownerLogPrefix + "|" + channelId.substring(1, channelId.length() - 1); + } + + @Override + protected void onRealConnect(ChannelHandlerContext ctx) { + LOG.debug("[{}] Starting channel initialization", logPrefix); + this.ctx = ctx; + new InitRequest(ctx).send(); + } + + @Override + protected boolean setConnectSuccess() { + boolean result = super.setConnectSuccess(); + if (result) { + // add heartbeat to pipeline now that protocol is initialized. + ctx.pipeline().addBefore("inflight", "heartbeat", heartbeatHandler); + } + return result; + } + + private enum Step { + STARTUP, + GET_CLUSTER_NAME, + SET_KEYSPACE, + AUTH_RESPONSE, + REGISTER, + } + + private class InitRequest extends ChannelHandlerRequest { + // This class is a finite-state automaton, that sends a different query depending on the step + // in the initialization sequence. + private Step step; + private Authenticator authenticator; + private ByteBuffer authReponseToken; + + InitRequest(ChannelHandlerContext ctx) { + super(ctx, timeoutMillis); + this.step = Step.STARTUP; + } + + @Override + String describe() { + return "[" + logPrefix + "] init query " + step; + } + + @Override + Message getRequest() { + switch (step) { + case STARTUP: + return new Startup(context.getStartupOptions()); + case GET_CLUSTER_NAME: + return CLUSTER_NAME_QUERY; + case SET_KEYSPACE: + return new Query("USE " + options.keyspace.asCql(false)); + case AUTH_RESPONSE: + return new AuthResponse(authReponseToken); + case REGISTER: + return new Register(options.eventTypes); + default: + throw new AssertionError("unhandled step: " + step); + } + } + + @Override + void onResponse(Message response) { + LOG.debug( + "[{}] step {} received response opcode={}", + logPrefix, + step, + ProtocolUtils.opcodeString(response.opcode)); + try { + if (step == Step.STARTUP && response instanceof Ready) { + context.getAuthProvider().ifPresent(provider -> provider.onMissingChallenge(endPoint)); + step = Step.GET_CLUSTER_NAME; + send(); + } else if (step == Step.STARTUP && response instanceof Authenticate) { + Authenticate authenticate = (Authenticate) response; + authenticator = buildAuthenticator(endPoint, authenticate.authenticator); + authenticator + .initialResponse() + .whenCompleteAsync( + (token, error) -> { + if (error != null) { + fail( + new AuthenticationException( + endPoint, "authenticator threw an exception", error)); + } else { + step = Step.AUTH_RESPONSE; + authReponseToken = token; + send(); + } + }, + channel.eventLoop()) + .exceptionally(UncaughtExceptions::log); + } else if (step == Step.AUTH_RESPONSE && response instanceof AuthChallenge) { + ByteBuffer challenge = ((AuthChallenge) response).token; + authenticator + .evaluateChallenge(challenge) + .whenCompleteAsync( + (token, error) -> { + if (error != null) { + fail( + new AuthenticationException( + endPoint, "authenticator threw an exception", error)); + } else { + step = Step.AUTH_RESPONSE; + authReponseToken = token; + send(); + } + }, + channel.eventLoop()) + .exceptionally(UncaughtExceptions::log); + } else if (step == Step.AUTH_RESPONSE && response instanceof AuthSuccess) { + ByteBuffer token = ((AuthSuccess) response).token; + authenticator + .onAuthenticationSuccess(token) + .whenCompleteAsync( + (ignored, error) -> { + if (error != null) { + fail( + new AuthenticationException( + endPoint, "authenticator threw an exception", error)); + } else { + step = Step.GET_CLUSTER_NAME; + send(); + } + }, + channel.eventLoop()) + .exceptionally(UncaughtExceptions::log); + } else if (step == Step.AUTH_RESPONSE + && response instanceof Error + && ((Error) response).code == ProtocolConstants.ErrorCode.AUTH_ERROR) { + fail( + new AuthenticationException( + endPoint, String.format("server replied '%s'", ((Error) response).message))); + } else if (step == Step.GET_CLUSTER_NAME && response instanceof Rows) { + Rows rows = (Rows) response; + List row = rows.getData().poll(); + String actualClusterName = getString(row, 0); + if (expectedClusterName != null && !expectedClusterName.equals(actualClusterName)) { + fail( + new ClusterNameMismatchException(endPoint, actualClusterName, expectedClusterName)); + } else { + if (expectedClusterName == null) { + // Store the actual name so that it can be retrieved from the factory + channel.attr(DriverChannel.CLUSTER_NAME_KEY).set(actualClusterName); + } + if (options.keyspace != null) { + step = Step.SET_KEYSPACE; + send(); + } else if (!options.eventTypes.isEmpty()) { + step = Step.REGISTER; + send(); + } else { + setConnectSuccess(); + } + } + } else if (step == Step.SET_KEYSPACE && response instanceof SetKeyspace) { + if (!options.eventTypes.isEmpty()) { + step = Step.REGISTER; + send(); + } else { + setConnectSuccess(); + } + } else if (step == Step.REGISTER && response instanceof Ready) { + setConnectSuccess(); + } else if (response instanceof Error) { + Error error = (Error) response; + // Testing for a specific string is a tad fragile but Cassandra doesn't give us a more + // precise error + // code. + // C* 2.1 reports a server error instead of protocol error, see CASSANDRA-9451. + if (step == Step.STARTUP + && (error.code == ProtocolConstants.ErrorCode.PROTOCOL_ERROR + || error.code == ProtocolConstants.ErrorCode.SERVER_ERROR) + && error.message.contains("Invalid or unsupported protocol version")) { + fail( + UnsupportedProtocolVersionException.forSingleAttempt( + endPoint, initialProtocolVersion)); + } else if (step == Step.SET_KEYSPACE + && error.code == ProtocolConstants.ErrorCode.INVALID) { + fail(new InvalidKeyspaceException(error.message)); + } else { + failOnUnexpected(error); + } + } else { + failOnUnexpected(response); + } + } catch (AuthenticationException e) { + fail(e); + } catch (Throwable t) { + fail("Unexpected exception at step " + step, t); + } + } + + @Override + void fail(String message, Throwable cause) { + Throwable finalException = + (message == null) ? cause : new ConnectionInitException(message, cause); + setConnectFailure(finalException); + } + + private Authenticator buildAuthenticator(EndPoint endPoint, String authenticator) { + return context + .getAuthProvider() + .map(p -> p.newAuthenticator(endPoint, authenticator)) + .orElseThrow( + () -> + new AuthenticationException( + endPoint, + String.format( + "Node %s requires authentication (%s), but no authenticator configured", + endPoint, authenticator))); + } + + @Override + public String toString() { + return "init query " + step; + } + } + + private String getString(List row, int i) { + return TypeCodecs.TEXT.decode(row.get(i), DefaultProtocolVersion.DEFAULT); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ResponseCallback.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ResponseCallback.java new file mode 100644 index 00000000000..e8fcd87247f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/ResponseCallback.java @@ -0,0 +1,79 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.protocol.internal.Frame; + +/** + * The outcome of a request sent to a Cassandra node. + * + *

This comes into play after the request has been successfully written to the channel. + * + *

Due to internal implementation constraints, different instances of this type must not be equal + * to each other (they are stored in a {@code BiMap} in {@link InFlightHandler}); reference equality + * should be appropriate in all cases. + */ +public interface ResponseCallback { + + /** + * Invoked when the server replies (note that the response frame might contain an error message). + */ + void onResponse(Frame responseFrame); + + /** + * Invoked if we couldn't get the response. + * + *

This can be triggered in two cases: + * + *

    + *
  • the connection was closed (for example, because of a heartbeat failure) before the + * response was received; + *
  • the response was received but there was an error while decoding it. + *
+ */ + void onFailure(Throwable error); + + /** + * Reports the stream id used for the request on the current connection. + * + *

This is called every time the request is written successfully to a connection (and therefore + * might multiple times in case of retries). It is guaranteed to be invoked before any response to + * the request on that connection is processed. + * + *

The default implementation does nothing. This only needs to be overridden for specialized + * requests that hold the stream id across multiple responses. + * + * @see #isLastResponse(Frame) + */ + default void onStreamIdAssigned(int streamId) { + // nothing to do + } + + /** + * Whether the given frame is the last response to this request. + * + *

This is invoked for each response received by this callback; if it returns {@code true}, the + * driver assumes that the server is no longer using this stream id, and that it can be safely + * reused to send another request. + * + *

The default implementation always returns {@code true}: regular CQL requests only have one + * response, and we can reuse the stream id as soon as we've received it. This only needs to be + * overridden for specialized requests that hold the stream id across multiple responses. + */ + default boolean isLastResponse(Frame responseFrame) { + return true; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/StreamIdGenerator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/StreamIdGenerator.java new file mode 100644 index 00000000000..77e985064b4 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/StreamIdGenerator.java @@ -0,0 +1,70 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import java.util.BitSet; +import net.jcip.annotations.NotThreadSafe; + +/** + * Manages the set of identifiers used to distinguish multiplexed requests on a channel. + * + *

This class is not thread safe: calls to {@link #acquire()} and {@link #release(int)} must be + * properly synchronized (in practice this is done by only calling them from the I/O thread). + * However, {@link #getAvailableIds()} has volatile semantics. + */ +@NotThreadSafe +class StreamIdGenerator { + + private final int maxAvailableIds; + // unset = available, set = borrowed (note that this is the opposite of the 3.x implementation) + private final BitSet ids; + private volatile int availableIds; + + StreamIdGenerator(int maxAvailableIds) { + this.maxAvailableIds = maxAvailableIds; + this.ids = new BitSet(this.maxAvailableIds); + this.availableIds = this.maxAvailableIds; + } + + @SuppressWarnings("NonAtomicVolatileUpdate") // see explanation in class Javadoc + int acquire() { + int id = ids.nextClearBit(0); + if (id >= maxAvailableIds) { + return -1; + } + ids.set(id); + availableIds--; + return id; + } + + @SuppressWarnings("NonAtomicVolatileUpdate") + void release(int id) { + if (ids.get(id)) { + availableIds++; + } else { + throw new IllegalStateException("Tried to release id that hadn't been borrowed: " + id); + } + ids.clear(id); + } + + int getAvailableIds() { + return availableIds; + } + + int getMaxAvailableIds() { + return maxAvailableIds; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/WriteCoalescer.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/WriteCoalescer.java new file mode 100644 index 00000000000..03fa691049d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/WriteCoalescer.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; + +/** + * Optimizes the write operations on Netty channels. + * + *

Flush operations are generally speaking expensive as these may trigger a syscall on the + * transport level. Thus it is in most cases (where write latency can be traded with throughput) a + * good idea to try to minimize flush operations as much as possible. This component allows writes + * to be accumulated and flushed together for better performance. + */ +public interface WriteCoalescer { + /** + * Writes and flushes the message to the channel, possibly at a later time, but the order of + * messages must be preserved. + */ + ChannelFuture writeAndFlush(Channel channel, Object message); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/channel/package-info.java b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/package-info.java new file mode 100644 index 00000000000..ecf6b6bc5af --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/channel/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** Handling of a single connection to a Cassandra node. */ +package com.datastax.oss.driver.internal.core.channel; diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/ConfigChangeEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/ConfigChangeEvent.java new file mode 100644 index 00000000000..a4dd8fd67c0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/ConfigChangeEvent.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.config; + +/** An event triggered when the configuration was changed. */ +public enum ConfigChangeEvent { + // Implementation note: to find where this event is consumed, look for references to the class + // itself, not INSTANCE (EventBus.register takes a class not an object). + INSTANCE +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/DriverOptionConfigBuilder.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/DriverOptionConfigBuilder.java new file mode 100644 index 00000000000..5c871d5a16d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/DriverOptionConfigBuilder.java @@ -0,0 +1,173 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.config; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.config.DriverOption; +import edu.umd.cs.findbugs.annotations.CheckReturnValue; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Duration; +import java.util.List; +import java.util.Map; + +/** + * A builder that allows specifying multiple configuration options keyed by {@link DriverOption}. + * + *

The intent of this interface is to reduce code duplication by sharing common {@code withXXX} + * methods among types that build configuration. It is not meant to be used to represent anything + * specifically. + * + *

The {@code withXXX} methods are similar to those found in {@link DriverExecutionProfile}, but + * do not share an interface as this interface is intended for internal implementations only. + * + *

It is not recommended to create implementations of this interface or use directly in code. + */ +public interface DriverOptionConfigBuilder { + + @NonNull + @CheckReturnValue + default SelfT withBoolean(@NonNull DriverOption option, boolean value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withBooleanList(@NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withInt(@NonNull DriverOption option, int value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withIntList(@NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withLong(@NonNull DriverOption option, long value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withLongList(@NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withDouble(@NonNull DriverOption option, double value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withDoubleList(@NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withString(@NonNull DriverOption option, @NonNull String value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withStringList(@NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @SuppressWarnings("unchecked") + @NonNull + @CheckReturnValue + default SelfT withStringMap(@NonNull DriverOption option, @NonNull Map value) { + SelfT v = (SelfT) this; + for (String key : value.keySet()) { + v = (SelfT) v.with(option.getPath() + "." + key, value.get(key)); + } + return v; + } + + /** + * Specifies a size in bytes. This is separate from {@link #withLong(DriverOption, long)}, in case + * implementations want to allow users to provide sizes in a more human-readable way, for example + * "256 MB". + */ + @NonNull + @CheckReturnValue + default SelfT withBytes(@NonNull DriverOption option, @NonNull String value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withBytes(@NonNull DriverOption option, long value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withBytesList(@NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withDuration(@NonNull DriverOption option, @NonNull Duration value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withDurationList(@NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @NonNull + @CheckReturnValue + default SelfT withClass(@NonNull DriverOption option, @NonNull Class value) { + return with(option, value.getName()); + } + + /** Unsets an option. */ + @NonNull + @CheckReturnValue + default SelfT without(@NonNull DriverOption option) { + return with(option, null); + } + + @NonNull + @CheckReturnValue + default SelfT with(@NonNull DriverOption option, @Nullable Object value) { + return with(option.getPath(), value); + } + + /** + * Provides a simple path to value mapping, all default methods invoke this method directly. It is + * not recommended that it is used directly other than by these defaults. + */ + @NonNull + @CheckReturnValue + SelfT with(@NonNull String path, @Nullable Object value); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/DefaultDriverConfigLoader.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/DefaultDriverConfigLoader.java new file mode 100644 index 00000000000..712b3db5388 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/DefaultDriverConfigLoader.java @@ -0,0 +1,233 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.config.typesafe; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import com.datastax.oss.driver.internal.core.config.ConfigChangeEvent; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.ScheduledFuture; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** The default loader; it is based on Typesafe Config and reloads at a configurable interval. */ +@ThreadSafe +public class DefaultDriverConfigLoader implements DriverConfigLoader { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultDriverConfigLoader.class); + + public static final Supplier DEFAULT_CONFIG_SUPPLIER = + () -> { + ConfigFactory.invalidateCaches(); + return ConfigFactory.load().getConfig("datastax-java-driver"); + }; + + private final Supplier configSupplier; + private final TypesafeDriverConfig driverConfig; + + private volatile SingleThreaded singleThreaded; + + /** + * Builds a new instance with the default Typesafe config loading rules (documented in {@link + * SessionBuilder#withConfigLoader(DriverConfigLoader)}) and the core driver options. + */ + public DefaultDriverConfigLoader() { + this(DEFAULT_CONFIG_SUPPLIER); + } + + /** + * Builds an instance with custom arguments, if you want to load the configuration from somewhere + * else. + */ + public DefaultDriverConfigLoader(Supplier configSupplier) { + this.configSupplier = configSupplier; + this.driverConfig = new TypesafeDriverConfig(configSupplier.get()); + } + + @NonNull + @Override + public DriverConfig getInitialConfig() { + return driverConfig; + } + + @Override + public void onDriverInit(@NonNull DriverContext driverContext) { + this.singleThreaded = new SingleThreaded((InternalDriverContext) driverContext); + } + + @NonNull + @Override + public CompletionStage reload() { + CompletableFuture result = new CompletableFuture<>(); + RunOrSchedule.on(singleThreaded.adminExecutor, () -> singleThreaded.reload(result)); + return result; + } + + @Override + public boolean supportsReloading() { + return true; + } + + /** For internal use only, this leaks a Typesafe config type. */ + @NonNull + public Supplier getConfigSupplier() { + return configSupplier; + } + + @Override + public void close() { + SingleThreaded singleThreaded = this.singleThreaded; + if (singleThreaded != null) { + RunOrSchedule.on(singleThreaded.adminExecutor, singleThreaded::close); + } + } + + /** + * Constructs a builder that may be used to provide additional configuration beyond those defined + * in your configuration files programmatically. For example: + * + *

{@code
+   * CqlSession session = CqlSession.builder()
+   *   .withConfigLoader(DefaultDriverConfigLoader.builder()
+   *     .withDuration(DefaultDriverOption.REQUEST_TIMEOUT, Duration.ofMillis(500))
+   *     .build())
+   *   .build();
+   * }
+ * + *

In the general case, use of this is not recommended, but it may be useful in situations + * where configuration must be defined at runtime or is derived from some other configuration + * source. + */ + @NonNull + public static DefaultDriverConfigLoaderBuilder builder() { + return new DefaultDriverConfigLoaderBuilder(); + } + + private class SingleThreaded { + private final String logPrefix; + private final EventExecutor adminExecutor; + private final EventBus eventBus; + private final DriverExecutionProfile config; + + private Duration reloadInterval; + private ScheduledFuture periodicTaskHandle; + private boolean closeWasCalled; + + private SingleThreaded(InternalDriverContext context) { + this.logPrefix = context.getSessionName(); + this.adminExecutor = context.getNettyOptions().adminEventExecutorGroup().next(); + this.eventBus = context.getEventBus(); + this.config = context.getConfig().getDefaultProfile(); + this.reloadInterval = + context + .getConfig() + .getDefaultProfile() + .getDuration(DefaultDriverOption.CONFIG_RELOAD_INTERVAL); + + RunOrSchedule.on(adminExecutor, this::schedulePeriodicReload); + } + + private void schedulePeriodicReload() { + assert adminExecutor.inEventLoop(); + // Cancel any previously running task + if (periodicTaskHandle != null) { + periodicTaskHandle.cancel(false); + } + if (reloadInterval.isZero()) { + LOG.debug("[{}] Reload interval is 0, disabling periodic reloading", logPrefix); + } else { + LOG.debug("[{}] Scheduling periodic reloading with interval {}", logPrefix, reloadInterval); + periodicTaskHandle = + adminExecutor.scheduleAtFixedRate( + this::reloadInBackground, + reloadInterval.toNanos(), + reloadInterval.toNanos(), + TimeUnit.NANOSECONDS); + } + } + + /** + * @param reloadedFuture a future to complete when the reload is complete (might be null if the + * caller is not interested in being notified) + */ + private void reload(CompletableFuture reloadedFuture) { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + if (reloadedFuture != null) { + reloadedFuture.completeExceptionally(new IllegalStateException("session is closing")); + } + return; + } + try { + boolean changed = driverConfig.reload(configSupplier.get()); + if (changed) { + LOG.info("[{}] Detected a configuration change", logPrefix); + eventBus.fire(ConfigChangeEvent.INSTANCE); + Duration newReloadInterval = + config.getDuration(DefaultDriverOption.CONFIG_RELOAD_INTERVAL); + if (!newReloadInterval.equals(reloadInterval)) { + reloadInterval = newReloadInterval; + schedulePeriodicReload(); + } + } else { + LOG.debug("[{}] Reloaded configuration but it hasn't changed", logPrefix); + } + if (reloadedFuture != null) { + reloadedFuture.complete(changed); + } + } catch (Error | RuntimeException e) { + if (reloadedFuture != null) { + reloadedFuture.completeExceptionally(e); + } else { + Loggers.warnWithException( + LOG, "[{}] Unexpected exception during scheduled reload", logPrefix, e); + } + } + } + + private void reloadInBackground() { + reload(null); + } + + private void close() { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + return; + } + closeWasCalled = true; + if (periodicTaskHandle != null) { + periodicTaskHandle.cancel(false); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/DefaultDriverConfigLoaderBuilder.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/DefaultDriverConfigLoaderBuilder.java new file mode 100644 index 00000000000..58b33c13cd9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/DefaultDriverConfigLoaderBuilder.java @@ -0,0 +1,123 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.config.typesafe; + +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.internal.core.config.DriverOptionConfigBuilder; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableMap; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValueFactory; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.NotThreadSafe; + +/** + * Provides a mechanism for constructing a {@link DriverConfigLoader} programmatically that uses + * {@link com.datastax.oss.driver.api.core.CqlSession}'s default config loader with the values of + * {@code withXXX(...)} methods overriding the configuration defined in configuration files. + * + *

The built {@link DriverConfigLoader} provided by {@link #build()} can be passed to {@link + * com.datastax.oss.driver.api.core.session.SessionBuilder#withConfigLoader(DriverConfigLoader)}. + */ +@NotThreadSafe +public class DefaultDriverConfigLoaderBuilder + implements DriverOptionConfigBuilder { + + private NullAllowingImmutableMap.Builder values = + NullAllowingImmutableMap.builder(); + + /** + * @return a new {@link ProfileBuilder} to provide programmatic configuration at a profile level. + * @see #withProfile(String, Profile) + */ + @NonNull + public static ProfileBuilder profileBuilder() { + return new ProfileBuilder(); + } + + /** Adds configuration for a profile constructed using {@link #profileBuilder()} by name. */ + @NonNull + public DefaultDriverConfigLoaderBuilder withProfile( + @NonNull String profileName, @NonNull Profile profile) { + String prefix = "profiles." + profileName + "."; + for (Map.Entry entry : profile.values.entrySet()) { + this.with(prefix + entry.getKey(), entry.getValue()); + } + return this; + } + + /** + * @return constructed {@link DriverConfigLoader} using the configuration passed into this + * builder. + */ + @NonNull + public DriverConfigLoader build() { + // fallback on the default config supplier (config file) + return new DefaultDriverConfigLoader( + () -> buildConfig().withFallback(DefaultDriverConfigLoader.DEFAULT_CONFIG_SUPPLIER.get())); + } + + /** @return A {@link Config} containing only the options provided */ + protected Config buildConfig() { + Config config = ConfigFactory.empty(); + for (Map.Entry entry : values.build().entrySet()) { + config = config.withValue(entry.getKey(), ConfigValueFactory.fromAnyRef(entry.getValue())); + } + return config; + } + + @NonNull + @Override + public DefaultDriverConfigLoaderBuilder with(@NonNull String path, @Nullable Object value) { + values.put(path, value); + return this; + } + + /** A builder for specifying options at a profile level using {@code withXXX} methods. */ + public static final class ProfileBuilder implements DriverOptionConfigBuilder { + + final NullAllowingImmutableMap.Builder values = + NullAllowingImmutableMap.builder(); + + private ProfileBuilder() {} + + @NonNull + @Override + public ProfileBuilder with(@NonNull String path, @Nullable Object value) { + values.put(path, value); + return this; + } + + @NonNull + public Profile build() { + return new Profile(values.build()); + } + } + + /** + * A single-purpose holder of profile options as a map to be consumed by {@link + * DefaultDriverConfigLoaderBuilder}. + */ + public static final class Profile { + final Map values; + + private Profile(Map values) { + this.values = values; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverConfig.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverConfig.java new file mode 100644 index 00000000000..8ed6b80dfd2 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverConfig.java @@ -0,0 +1,144 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.config.typesafe; + +import static com.typesafe.config.ConfigValueType.OBJECT; + +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigObject; +import com.typesafe.config.ConfigValue; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class TypesafeDriverConfig implements DriverConfig { + + private static final Logger LOG = LoggerFactory.getLogger(TypesafeDriverConfig.class); + + private final ImmutableMap profiles; + // Only used to detect if reload saw any change + private volatile Config lastLoadedConfig; + + public TypesafeDriverConfig(Config config) { + this.lastLoadedConfig = config; + + Map profileConfigs = extractProfiles(config); + + ImmutableMap.Builder builder = + ImmutableMap.builder(); + for (Map.Entry entry : profileConfigs.entrySet()) { + builder.put( + entry.getKey(), + new TypesafeDriverExecutionProfile.Base(entry.getKey(), entry.getValue())); + } + this.profiles = builder.build(); + } + + /** @return whether the configuration changed */ + public boolean reload(Config config) { + if (config.equals(lastLoadedConfig)) { + return false; + } else { + lastLoadedConfig = config; + try { + Map profileConfigs = extractProfiles(config); + for (Map.Entry entry : profileConfigs.entrySet()) { + String profileName = entry.getKey(); + TypesafeDriverExecutionProfile.Base profile = this.profiles.get(profileName); + if (profile == null) { + LOG.warn( + "Unknown profile '{}' while reloading configuration. " + + "Adding profiles at runtime is not supported.", + profileName); + } else { + profile.refresh(entry.getValue()); + } + } + return true; + } catch (Throwable t) { + Loggers.warnWithException(LOG, "Error reloading configuration, keeping previous one", t); + return false; + } + } + } + + /* + * Processes the raw configuration to extract profiles. For example: + * { + * foo = 1, bar = 2 + * profiles { + * custom1 { bar = 3 } + * } + * } + * Would produce: + * "default" => { foo = 1, bar = 2 } + * "custom1" => { foo = 1, bar = 3 } + */ + private Map extractProfiles(Config sourceConfig) { + ImmutableMap.Builder result = ImmutableMap.builder(); + + Config defaultProfileConfig = sourceConfig.withoutPath("profiles"); + result.put(DriverExecutionProfile.DEFAULT_NAME, defaultProfileConfig); + + // The rest of the method is a bit confusing because we navigate between Typesafe config's two + // APIs, see https://github.com/typesafehub/config#understanding-config-and-configobject + // In an attempt to clarify: + // xxxObject = `ConfigObject` API (config as a hierarchical structure) + // xxxConfig = `Config` API (config as a flat set of options with hierarchical paths) + ConfigObject rootObject = sourceConfig.root(); + if (rootObject.containsKey("profiles") && rootObject.get("profiles").valueType() == OBJECT) { + ConfigObject profilesObject = (ConfigObject) rootObject.get("profiles"); + for (String profileName : profilesObject.keySet()) { + if (profileName.equals(DriverExecutionProfile.DEFAULT_NAME)) { + throw new IllegalArgumentException( + String.format( + "Can't have %s as a profile name because it's used internally. Pick another name.", + profileName)); + } + ConfigValue profileObject = profilesObject.get(profileName); + if (profileObject.valueType() == OBJECT) { + Config profileConfig = ((ConfigObject) profileObject).toConfig(); + result.put(profileName, profileConfig.withFallback(defaultProfileConfig)); + } + } + } + return result.build(); + } + + @NonNull + @Override + public DriverExecutionProfile getProfile(@NonNull String profileName) { + Preconditions.checkArgument( + profiles.containsKey(profileName), + "Unknown profile '%s'. Check your configuration.", + profileName); + return profiles.get(profileName); + } + + @NonNull + @Override + public Map getProfiles() { + return profiles; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverExecutionProfile.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverExecutionProfile.java new file mode 100644 index 00000000000..31275a4acce --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverExecutionProfile.java @@ -0,0 +1,411 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.config.typesafe; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.config.DriverOption; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSortedSet; +import com.datastax.oss.driver.shaded.guava.common.collect.MapMaker; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValue; +import com.typesafe.config.ConfigValueFactory; +import com.typesafe.config.ConfigValueType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.time.Duration; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public abstract class TypesafeDriverExecutionProfile implements DriverExecutionProfile { + + /** The original profile in the driver's configuration that this profile was derived from. */ + protected abstract Base getBaseProfile(); + + /** The extra options that were added with {@code withXxx} methods. */ + protected abstract Config getAddedOptions(); + + /** The actual options that will be used to answer {@code getXxx} calls. */ + protected abstract Config getEffectiveOptions(); + + protected final ConcurrentMap cache = new ConcurrentHashMap<>(); + + @Override + public boolean isDefined(@NonNull DriverOption option) { + return getEffectiveOptions().hasPath(option.getPath()); + } + + @Override + public boolean getBoolean(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getBoolean); + } + + @NonNull + @Override + public DriverExecutionProfile withBoolean(@NonNull DriverOption option, boolean value) { + return with(option, value); + } + + @NonNull + @Override + public List getBooleanList(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getBooleanList); + } + + @NonNull + @Override + public DriverExecutionProfile withBooleanList( + @NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @Override + public int getInt(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getInt); + } + + @NonNull + @Override + public DriverExecutionProfile withInt(@NonNull DriverOption option, int value) { + return with(option, value); + } + + @NonNull + @Override + public List getIntList(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getIntList); + } + + @NonNull + @Override + public DriverExecutionProfile withIntList( + @NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @Override + public long getLong(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getLong); + } + + @NonNull + @Override + public DriverExecutionProfile withLong(@NonNull DriverOption option, long value) { + return with(option, value); + } + + @NonNull + @Override + public List getLongList(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getLongList); + } + + @NonNull + @Override + public DriverExecutionProfile withLongList( + @NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @Override + public double getDouble(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getDouble); + } + + @NonNull + @Override + public DriverExecutionProfile withDouble(@NonNull DriverOption option, double value) { + return with(option, value); + } + + @NonNull + @Override + public List getDoubleList(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getDoubleList); + } + + @NonNull + @Override + public DriverExecutionProfile withDoubleList( + @NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @NonNull + @Override + public String getString(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getString); + } + + @NonNull + @Override + public DriverExecutionProfile withString(@NonNull DriverOption option, @NonNull String value) { + return with(option, value); + } + + @NonNull + @Override + public List getStringList(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getStringList); + } + + @NonNull + @Override + public DriverExecutionProfile withStringList( + @NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @NonNull + @Override + public Map getStringMap(@NonNull DriverOption option) { + Config subConfig = getCached(option.getPath(), getEffectiveOptions()::getConfig); + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : subConfig.entrySet()) { + if (entry.getValue().valueType().equals(ConfigValueType.STRING)) { + builder.put(entry.getKey(), (String) entry.getValue().unwrapped()); + } + } + return builder.build(); + } + + @NonNull + @Override + public DriverExecutionProfile withStringMap( + @NonNull DriverOption option, @NonNull Map map) { + Base base = getBaseProfile(); + // Add the new option to any already derived options + Config newAdded = getAddedOptions(); + for (String key : map.keySet()) { + newAdded = + newAdded.withValue( + option.getPath() + "." + key, ConfigValueFactory.fromAnyRef(map.get(key))); + } + Derived derived = new Derived(base, newAdded); + base.register(derived); + return derived; + } + + @Override + public long getBytes(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getBytes); + } + + @NonNull + @Override + public DriverExecutionProfile withBytes(@NonNull DriverOption option, long value) { + return with(option, value); + } + + @NonNull + @Override + public List getBytesList(DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getBytesList); + } + + @NonNull + @Override + public DriverExecutionProfile withBytesList( + @NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @NonNull + @Override + public Duration getDuration(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getDuration); + } + + @NonNull + @Override + public DriverExecutionProfile withDuration( + @NonNull DriverOption option, @NonNull Duration value) { + return with(option, value); + } + + @NonNull + @Override + public List getDurationList(@NonNull DriverOption option) { + return getCached(option.getPath(), getEffectiveOptions()::getDurationList); + } + + @NonNull + @Override + public DriverExecutionProfile withDurationList( + @NonNull DriverOption option, @NonNull List value) { + return with(option, value); + } + + @NonNull + @Override + public DriverExecutionProfile without(@NonNull DriverOption option) { + return with(option, null); + } + + @NonNull + @Override + public Object getComparisonKey(@NonNull DriverOption option) { + // No need to cache this, it's only used for policy initialization + return getEffectiveOptions().getConfig(option.getPath()); + } + + @NonNull + @Override + public SortedSet> entrySet() { + ImmutableSortedSet.Builder> builder = + ImmutableSortedSet.orderedBy(Comparator.comparing(Map.Entry::getKey)); + for (Map.Entry entry : getEffectiveOptions().entrySet()) { + builder.add(new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().unwrapped())); + } + return builder.build(); + } + + private T getCached(String path, Function compute) { + // compute's signature guarantees we get a T, and this is the only place where we mutate the + // entry + @SuppressWarnings("unchecked") + T t = (T) cache.computeIfAbsent(path, compute); + return t; + } + + private DriverExecutionProfile with(@NonNull DriverOption option, @Nullable Object value) { + Base base = getBaseProfile(); + // Add the new option to any already derived options + Config newAdded = + getAddedOptions().withValue(option.getPath(), ConfigValueFactory.fromAnyRef(value)); + Derived derived = new Derived(base, newAdded); + base.register(derived); + return derived; + } + + /** A profile that was loaded directly from the driver's configuration. */ + @ThreadSafe + static class Base extends TypesafeDriverExecutionProfile { + + private final String name; + private volatile Config options; + private volatile Set derivedProfiles; + + Base(String name, Config options) { + this.name = name; + this.options = options; + } + + @NonNull + @Override + public String getName() { + return name; + } + + @Override + protected Base getBaseProfile() { + return this; + } + + @Override + protected Config getAddedOptions() { + return ConfigFactory.empty(); + } + + @Override + protected Config getEffectiveOptions() { + return options; + } + + void refresh(Config newOptions) { + this.options = newOptions; + this.cache.clear(); + if (derivedProfiles != null) { + for (Derived derivedProfile : derivedProfiles) { + derivedProfile.refresh(); + } + } + } + + void register(Derived derivedProfile) { + getDerivedProfiles().add(derivedProfile); + } + + // Lazy init + private Set getDerivedProfiles() { + Set result = derivedProfiles; + if (result == null) { + synchronized (this) { + result = derivedProfiles; + if (result == null) { + derivedProfiles = + result = Collections.newSetFromMap(new MapMaker().weakKeys().makeMap()); + } + } + } + return result; + } + } + + /** + * A profile that was copied from another profile programmatically using {@code withXxx} methods. + */ + @ThreadSafe + static class Derived extends TypesafeDriverExecutionProfile { + + private final Base baseProfile; + private final Config addedOptions; + private volatile Config effectiveOptions; + + Derived(Base baseProfile, Config addedOptions) { + this.baseProfile = baseProfile; + this.addedOptions = addedOptions; + refresh(); + } + + void refresh() { + this.effectiveOptions = addedOptions.withFallback(baseProfile.getEffectiveOptions()); + this.cache.clear(); + } + + @NonNull + @Override + public String getName() { + return baseProfile.getName(); + } + + @Override + protected Base getBaseProfile() { + return baseProfile; + } + + @Override + protected Config getAddedOptions() { + return addedOptions; + } + + @Override + protected Config getEffectiveOptions() { + return effectiveOptions; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/package-info.java b/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/package-info.java new file mode 100644 index 00000000000..f59350afff5 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/config/typesafe/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Implementation of the driver configuration based on the Typesafe config library. + */ +package com.datastax.oss.driver.internal.core.config.typesafe; diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/connection/ConstantReconnectionPolicy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/connection/ConstantReconnectionPolicy.java new file mode 100644 index 00000000000..ccead526906 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/connection/ConstantReconnectionPolicy.java @@ -0,0 +1,87 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.connection; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Node; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A reconnection policy that waits a constant time between each reconnection attempt. + * + *

To activate this policy, modify the {@code advanced.reconnection-policy} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.reconnection-policy {
+ *     class = ConstantReconnectionPolicy
+ *     base-delay = 1 second
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +public class ConstantReconnectionPolicy implements ReconnectionPolicy { + + private static final Logger LOG = LoggerFactory.getLogger(ConstantReconnectionPolicy.class); + + private final String logPrefix; + private final ReconnectionSchedule schedule; + + /** Builds a new instance. */ + public ConstantReconnectionPolicy(DriverContext context) { + this.logPrefix = context.getSessionName(); + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + Duration delay = config.getDuration(DefaultDriverOption.RECONNECTION_BASE_DELAY); + if (delay.isNegative()) { + throw new IllegalArgumentException( + String.format( + "Invalid negative delay for " + + DefaultDriverOption.RECONNECTION_BASE_DELAY.getPath() + + " (got %d)", + delay)); + } + this.schedule = () -> delay; + } + + @NonNull + @Override + public ReconnectionSchedule newNodeSchedule(@NonNull Node node) { + LOG.debug("[{}] Creating new schedule for {}", logPrefix, node); + return schedule; + } + + @NonNull + @Override + public ReconnectionSchedule newControlConnectionSchedule( + @SuppressWarnings("ignored") boolean isInitialConnection) { + LOG.debug("[{}] Creating new schedule for the control connection", logPrefix); + return schedule; + } + + @Override + public void close() { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/connection/ExponentialReconnectionPolicy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/connection/ExponentialReconnectionPolicy.java new file mode 100644 index 00000000000..2320dee255a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/connection/ExponentialReconnectionPolicy.java @@ -0,0 +1,167 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.connection; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import java.util.concurrent.ThreadLocalRandom; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A reconnection policy that waits exponentially longer between each reconnection attempt (but + * keeps a constant delay once a maximum delay is reached). + * + *

It uses the same schedule implementation for individual nodes or the control connection: + * reconnection attempt {@code i} will be tried {@code Math.min(2^(i-1) * getBaseDelayMs(), + * getMaxDelayMs())} milliseconds after the previous one. A random amount of jitter (+/- 15%) will + * be added to the pure exponential delay value to avoid situations where many clients are in the + * reconnection process at exactly the same time. The jitter will never cause the delay to be less + * than the base delay, or more than the max delay. + * + *

To activate this policy, modify the {@code advanced.reconnection-policy} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.reconnection-policy {
+ *     class = ExponentialReconnectionPolicy
+ *     base-delay = 1 second
+ *     max-delay = 60 seconds
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class ExponentialReconnectionPolicy implements ReconnectionPolicy { + + private static final Logger LOG = LoggerFactory.getLogger(ExponentialReconnectionPolicy.class); + + private final String logPrefix; + private final long baseDelayMs; + private final long maxDelayMs; + private final long maxAttempts; + + /** Builds a new instance. */ + public ExponentialReconnectionPolicy(DriverContext context) { + this.logPrefix = context.getSessionName(); + + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + + this.baseDelayMs = config.getDuration(DefaultDriverOption.RECONNECTION_BASE_DELAY).toMillis(); + this.maxDelayMs = config.getDuration(DefaultDriverOption.RECONNECTION_MAX_DELAY).toMillis(); + + Preconditions.checkArgument( + baseDelayMs > 0, + "%s must be strictly positive (got %s)", + DefaultDriverOption.RECONNECTION_BASE_DELAY.getPath(), + baseDelayMs); + Preconditions.checkArgument( + maxDelayMs >= 0, + "%s must be positive (got %s)", + DefaultDriverOption.RECONNECTION_MAX_DELAY.getPath(), + maxDelayMs); + Preconditions.checkArgument( + maxDelayMs >= baseDelayMs, + "%s must be bigger than %s (got %s, %s)", + DefaultDriverOption.RECONNECTION_MAX_DELAY.getPath(), + DefaultDriverOption.RECONNECTION_BASE_DELAY.getPath(), + maxDelayMs, + baseDelayMs); + + // Maximum number of attempts after which we overflow + int ceil = (baseDelayMs & (baseDelayMs - 1)) == 0 ? 0 : 1; + this.maxAttempts = 64L - Long.numberOfLeadingZeros(Long.MAX_VALUE / baseDelayMs) - ceil; + } + + /** + * The base delay in milliseconds for this policy (e.g. the delay before the first reconnection + * attempt). + * + * @return the base delay in milliseconds for this policy. + */ + public long getBaseDelayMs() { + return baseDelayMs; + } + + /** + * The maximum delay in milliseconds between reconnection attempts for this policy. + * + * @return the maximum delay in milliseconds between reconnection attempts for this policy. + */ + public long getMaxDelayMs() { + return maxDelayMs; + } + + @NonNull + @Override + public ReconnectionSchedule newNodeSchedule(@NonNull Node node) { + LOG.debug("[{}] Creating new schedule for {}", logPrefix, node); + return new ExponentialSchedule(); + } + + @NonNull + @Override + public ReconnectionSchedule newControlConnectionSchedule( + @SuppressWarnings("ignored") boolean isInitialConnection) { + LOG.debug("[{}] Creating new schedule for the control connection", logPrefix); + return new ExponentialSchedule(); + } + + @Override + public void close() { + // nothing to do + } + + private class ExponentialSchedule implements ReconnectionSchedule { + + private int attempts; + + @NonNull + @Override + public Duration nextDelay() { + long delay = (attempts > maxAttempts) ? maxDelayMs : calculateDelayWithJitter(); + return Duration.ofMillis(delay); + } + + private long calculateDelayWithJitter() { + // assert we haven't hit the max attempts + assert attempts <= maxAttempts; + // get the pure exponential delay based on the attempt count + long delay = Math.min(baseDelayMs * (1L << attempts++), maxDelayMs); + // calculate up to 15% jitter, plus or minus (i.e. 85 - 115% of the pure value) + int jitter = ThreadLocalRandom.current().nextInt(85, 116); + // apply jitter + delay = (jitter * delay) / 100; + // ensure the final delay is between the base and max + delay = Math.min(maxDelayMs, Math.max(baseDelayMs, delay)); + return delay; + } + } + + public long getMaxAttempts() { + return maxAttempts; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java new file mode 100644 index 00000000000..2b3d03d22f7 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultDriverContext.java @@ -0,0 +1,773 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.context; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; +import com.datastax.oss.driver.api.core.auth.AuthProvider; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; +import com.datastax.oss.driver.api.core.time.TimestampGenerator; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.CassandraProtocolVersionRegistry; +import com.datastax.oss.driver.internal.core.ConsistencyLevelRegistry; +import com.datastax.oss.driver.internal.core.DefaultConsistencyLevelRegistry; +import com.datastax.oss.driver.internal.core.ProtocolVersionRegistry; +import com.datastax.oss.driver.internal.core.channel.ChannelFactory; +import com.datastax.oss.driver.internal.core.channel.DefaultWriteCoalescer; +import com.datastax.oss.driver.internal.core.channel.WriteCoalescer; +import com.datastax.oss.driver.internal.core.control.ControlConnection; +import com.datastax.oss.driver.internal.core.metadata.DefaultTopologyMonitor; +import com.datastax.oss.driver.internal.core.metadata.LoadBalancingPolicyWrapper; +import com.datastax.oss.driver.internal.core.metadata.MetadataManager; +import com.datastax.oss.driver.internal.core.metadata.TopologyMonitor; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.DefaultSchemaParserFactory; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.SchemaParserFactory; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.DefaultSchemaQueriesFactory; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaQueriesFactory; +import com.datastax.oss.driver.internal.core.metadata.token.DefaultReplicationStrategyFactory; +import com.datastax.oss.driver.internal.core.metadata.token.DefaultTokenFactoryRegistry; +import com.datastax.oss.driver.internal.core.metadata.token.ReplicationStrategyFactory; +import com.datastax.oss.driver.internal.core.metadata.token.TokenFactoryRegistry; +import com.datastax.oss.driver.internal.core.metrics.DropwizardMetricsFactory; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.internal.core.pool.ChannelPoolFactory; +import com.datastax.oss.driver.internal.core.protocol.ByteBufPrimitiveCodec; +import com.datastax.oss.driver.internal.core.protocol.Lz4Compressor; +import com.datastax.oss.driver.internal.core.protocol.SnappyCompressor; +import com.datastax.oss.driver.internal.core.servererrors.DefaultWriteTypeRegistry; +import com.datastax.oss.driver.internal.core.servererrors.WriteTypeRegistry; +import com.datastax.oss.driver.internal.core.session.PoolManager; +import com.datastax.oss.driver.internal.core.session.RequestProcessorRegistry; +import com.datastax.oss.driver.internal.core.ssl.JdkSslHandlerFactory; +import com.datastax.oss.driver.internal.core.ssl.SslHandlerFactory; +import com.datastax.oss.driver.internal.core.tracker.RequestLogFormatter; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; +import com.datastax.oss.driver.internal.core.util.Reflection; +import com.datastax.oss.driver.internal.core.util.concurrent.CycleDetector; +import com.datastax.oss.driver.internal.core.util.concurrent.LazyReference; +import com.datastax.oss.protocol.internal.Compressor; +import com.datastax.oss.protocol.internal.FrameCodec; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import io.netty.buffer.ByteBuf; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import net.jcip.annotations.ThreadSafe; + +/** + * Default implementation of the driver context. + * + *

All non-constant components are initialized lazily. Some components depend on others, so there + * might be deadlocks or stack overflows if the dependency graph is badly designed. This can be + * checked automatically with the system property {@code + * -Dcom.datastax.oss.driver.DETECT_CYCLES=true} (this might have a slight impact on startup time, + * so the check is disabled by default). + * + *

This is DIY dependency injection. We stayed away from DI frameworks for simplicity, to avoid + * an extra dependency, and because end users might want to access some of these components in their + * own implementations (which wouldn't work well with compile-time approaches like Dagger). + * + *

This also provides extension points for stuff that is too low-level for the driver + * configuration: the intent is that someone can extend this class, override one (or more) of the + * buildXxx methods, and initialize the cluster with this new implementation. + */ +@ThreadSafe +public class DefaultDriverContext implements InternalDriverContext { + + private static final AtomicInteger SESSION_NAME_COUNTER = new AtomicInteger(); + + private final CycleDetector cycleDetector = + new CycleDetector("Detected cycle in context initialization"); + + private final LazyReference> loadBalancingPoliciesRef = + new LazyReference<>("loadBalancingPolicies", this::buildLoadBalancingPolicies, cycleDetector); + private final LazyReference reconnectionPolicyRef = + new LazyReference<>("reconnectionPolicy", this::buildReconnectionPolicy, cycleDetector); + private final LazyReference> retryPoliciesRef = + new LazyReference<>("retryPolicies", this::buildRetryPolicies, cycleDetector); + private final LazyReference> + speculativeExecutionPoliciesRef = + new LazyReference<>( + "speculativeExecutionPolicies", + this::buildSpeculativeExecutionPolicies, + cycleDetector); + private final LazyReference timestampGeneratorRef = + new LazyReference<>("timestampGenerator", this::buildTimestampGenerator, cycleDetector); + private final LazyReference addressTranslatorRef = + new LazyReference<>("addressTranslator", this::buildAddressTranslator, cycleDetector); + private final LazyReference> authProviderRef = + new LazyReference<>("authProvider", this::buildAuthProvider, cycleDetector); + private final LazyReference> sslEngineFactoryRef = + new LazyReference<>("sslEngineFactory", this::buildSslEngineFactory, cycleDetector); + + private final LazyReference eventBusRef = + new LazyReference<>("eventBus", this::buildEventBus, cycleDetector); + private final LazyReference> compressorRef = + new LazyReference<>("compressor", this::buildCompressor, cycleDetector); + private final LazyReference> frameCodecRef = + new LazyReference<>("frameCodec", this::buildFrameCodec, cycleDetector); + private final LazyReference protocolVersionRegistryRef = + new LazyReference<>( + "protocolVersionRegistry", this::buildProtocolVersionRegistry, cycleDetector); + private final LazyReference consistencyLevelRegistryRef = + new LazyReference<>( + "consistencyLevelRegistry", this::buildConsistencyLevelRegistry, cycleDetector); + private final LazyReference writeTypeRegistryRef = + new LazyReference<>("writeTypeRegistry", this::buildWriteTypeRegistry, cycleDetector); + private final LazyReference nettyOptionsRef = + new LazyReference<>("nettyOptions", this::buildNettyOptions, cycleDetector); + private final LazyReference writeCoalescerRef = + new LazyReference<>("writeCoalescer", this::buildWriteCoalescer, cycleDetector); + private final LazyReference> sslHandlerFactoryRef = + new LazyReference<>("sslHandlerFactory", this::buildSslHandlerFactory, cycleDetector); + private final LazyReference channelFactoryRef = + new LazyReference<>("channelFactory", this::buildChannelFactory, cycleDetector); + private final LazyReference topologyMonitorRef = + new LazyReference<>("topologyMonitor", this::buildTopologyMonitor, cycleDetector); + private final LazyReference metadataManagerRef = + new LazyReference<>("metadataManager", this::buildMetadataManager, cycleDetector); + private final LazyReference loadBalancingPolicyWrapperRef = + new LazyReference<>( + "loadBalancingPolicyWrapper", this::buildLoadBalancingPolicyWrapper, cycleDetector); + private final LazyReference controlConnectionRef = + new LazyReference<>("controlConnection", this::buildControlConnection, cycleDetector); + private final LazyReference requestProcessorRegistryRef = + new LazyReference<>( + "requestProcessorRegistry", this::buildRequestProcessorRegistry, cycleDetector); + private final LazyReference schemaQueriesFactoryRef = + new LazyReference<>("schemaQueriesFactory", this::buildSchemaQueriesFactory, cycleDetector); + private final LazyReference schemaParserFactoryRef = + new LazyReference<>("schemaParserFactory", this::buildSchemaParserFactory, cycleDetector); + private final LazyReference tokenFactoryRegistryRef = + new LazyReference<>("tokenFactoryRegistry", this::buildTokenFactoryRegistry, cycleDetector); + private final LazyReference replicationStrategyFactoryRef = + new LazyReference<>( + "replicationStrategyFactory", this::buildReplicationStrategyFactory, cycleDetector); + private final LazyReference poolManagerRef = + new LazyReference<>("poolManager", this::buildPoolManager, cycleDetector); + private final LazyReference metricsFactoryRef = + new LazyReference<>("metricsFactory", this::buildMetricsFactory, cycleDetector); + private final LazyReference requestThrottlerRef = + new LazyReference<>("requestThrottler", this::buildRequestThrottler, cycleDetector); + private final LazyReference> startupOptionsRef = + new LazyReference<>("startupOptions", this::buildStartupOptions, cycleDetector); + private final LazyReference nodeStateListenerRef; + private final LazyReference schemaChangeListenerRef; + private final LazyReference requestTrackerRef; + + private final DriverConfig config; + private final DriverConfigLoader configLoader; + private final ChannelPoolFactory channelPoolFactory = new ChannelPoolFactory(); + private final CodecRegistry codecRegistry; + private final String sessionName; + private final NodeStateListener nodeStateListenerFromBuilder; + private final SchemaChangeListener schemaChangeListenerFromBuilder; + private final RequestTracker requestTrackerFromBuilder; + private final Map localDatacentersFromBuilder; + private final Map> nodeFiltersFromBuilder; + private final ClassLoader classLoader; + private final LazyReference requestLogFormatterRef = + new LazyReference<>("requestLogFormatter", this::buildRequestLogFormatter, cycleDetector); + + public DefaultDriverContext( + DriverConfigLoader configLoader, + List> typeCodecs, + NodeStateListener nodeStateListener, + SchemaChangeListener schemaChangeListener, + RequestTracker requestTracker, + Map localDatacenters, + Map> nodeFilters, + ClassLoader classLoader) { + this.config = configLoader.getInitialConfig(); + this.configLoader = configLoader; + DriverExecutionProfile defaultProfile = config.getDefaultProfile(); + if (defaultProfile.isDefined(DefaultDriverOption.SESSION_NAME)) { + this.sessionName = defaultProfile.getString(DefaultDriverOption.SESSION_NAME); + } else { + this.sessionName = "s" + SESSION_NAME_COUNTER.getAndIncrement(); + } + this.localDatacentersFromBuilder = localDatacenters; + this.codecRegistry = buildCodecRegistry(this.sessionName, typeCodecs); + this.nodeStateListenerFromBuilder = nodeStateListener; + this.nodeStateListenerRef = + new LazyReference<>( + "nodeStateListener", + () -> buildNodeStateListener(nodeStateListenerFromBuilder), + cycleDetector); + this.schemaChangeListenerFromBuilder = schemaChangeListener; + this.schemaChangeListenerRef = + new LazyReference<>( + "schemaChangeListener", + () -> buildSchemaChangeListener(schemaChangeListenerFromBuilder), + cycleDetector); + this.requestTrackerFromBuilder = requestTracker; + this.requestTrackerRef = + new LazyReference<>( + "requestTracker", () -> buildRequestTracker(requestTrackerFromBuilder), cycleDetector); + this.nodeFiltersFromBuilder = nodeFilters; + this.classLoader = classLoader; + } + + /** + * Builds a map of options to send in a Startup message. + * + * @see #getStartupOptions() + */ + protected Map buildStartupOptions() { + return new StartupOptionsBuilder(this).build(); + } + + protected Map buildLoadBalancingPolicies() { + return Reflection.buildFromConfigProfiles( + this, + DefaultDriverOption.LOAD_BALANCING_POLICY, + LoadBalancingPolicy.class, + "com.datastax.oss.driver.internal.core.loadbalancing"); + } + + protected Map buildRetryPolicies() { + return Reflection.buildFromConfigProfiles( + this, + DefaultDriverOption.RETRY_POLICY, + RetryPolicy.class, + "com.datastax.oss.driver.internal.core.retry"); + } + + protected Map buildSpeculativeExecutionPolicies() { + return Reflection.buildFromConfigProfiles( + this, + DefaultDriverOption.SPECULATIVE_EXECUTION_POLICY, + SpeculativeExecutionPolicy.class, + "com.datastax.oss.driver.internal.core.specex"); + } + + protected TimestampGenerator buildTimestampGenerator() { + return Reflection.buildFromConfig( + this, + DefaultDriverOption.TIMESTAMP_GENERATOR_CLASS, + TimestampGenerator.class, + "com.datastax.oss.driver.internal.core.time") + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Missing timestamp generator, check your configuration (%s)", + DefaultDriverOption.TIMESTAMP_GENERATOR_CLASS))); + } + + protected ReconnectionPolicy buildReconnectionPolicy() { + return Reflection.buildFromConfig( + this, + DefaultDriverOption.RECONNECTION_POLICY_CLASS, + ReconnectionPolicy.class, + "com.datastax.oss.driver.internal.core.connection") + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Missing reconnection policy, check your configuration (%s)", + DefaultDriverOption.RECONNECTION_POLICY_CLASS))); + } + + protected AddressTranslator buildAddressTranslator() { + return Reflection.buildFromConfig( + this, + DefaultDriverOption.ADDRESS_TRANSLATOR_CLASS, + AddressTranslator.class, + "com.datastax.oss.driver.internal.core.addresstranslation") + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Missing address translator, check your configuration (%s)", + DefaultDriverOption.ADDRESS_TRANSLATOR_CLASS))); + } + + protected Optional buildAuthProvider() { + return Reflection.buildFromConfig( + this, + DefaultDriverOption.AUTH_PROVIDER_CLASS, + AuthProvider.class, + "com.datastax.oss.driver.internal.core.auth"); + } + + protected Optional buildSslEngineFactory() { + return Reflection.buildFromConfig( + this, + DefaultDriverOption.SSL_ENGINE_FACTORY_CLASS, + SslEngineFactory.class, + "com.datastax.oss.driver.internal.core.ssl"); + } + + protected EventBus buildEventBus() { + return new EventBus(getSessionName()); + } + + @SuppressWarnings("unchecked") + protected Compressor buildCompressor() { + DriverExecutionProfile defaultProfile = getConfig().getDefaultProfile(); + if (defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_COMPRESSION)) { + String name = defaultProfile.getString(DefaultDriverOption.PROTOCOL_COMPRESSION); + if (name.equalsIgnoreCase("lz4")) { + return new Lz4Compressor(this); + } else if (name.equalsIgnoreCase("snappy")) { + return new SnappyCompressor(this); + } else { + throw new IllegalArgumentException( + String.format( + "Unsupported compression algorithm '%s' (from configuration option %s)", + name, DefaultDriverOption.PROTOCOL_COMPRESSION.getPath())); + } + } else { + return Compressor.none(); + } + } + + protected FrameCodec buildFrameCodec() { + return FrameCodec.defaultClient( + new ByteBufPrimitiveCodec(getNettyOptions().allocator()), getCompressor()); + } + + protected ProtocolVersionRegistry buildProtocolVersionRegistry() { + return new CassandraProtocolVersionRegistry(getSessionName()); + } + + protected ConsistencyLevelRegistry buildConsistencyLevelRegistry() { + return new DefaultConsistencyLevelRegistry(); + } + + protected WriteTypeRegistry buildWriteTypeRegistry() { + return new DefaultWriteTypeRegistry(); + } + + protected NettyOptions buildNettyOptions() { + return new DefaultNettyOptions(this); + } + + protected Optional buildSslHandlerFactory() { + // If a JDK-based factory was provided through the public API, syncWrapper it + return buildSslEngineFactory().map(JdkSslHandlerFactory::new); + + // For more advanced options (like using Netty's native OpenSSL support instead of the JDK), + // extend DefaultDriverContext and override this method + } + + protected WriteCoalescer buildWriteCoalescer() { + return new DefaultWriteCoalescer(this); + } + + protected ChannelFactory buildChannelFactory() { + return new ChannelFactory(this); + } + + protected TopologyMonitor buildTopologyMonitor() { + return new DefaultTopologyMonitor(this); + } + + protected MetadataManager buildMetadataManager() { + return new MetadataManager(this); + } + + protected LoadBalancingPolicyWrapper buildLoadBalancingPolicyWrapper() { + return new LoadBalancingPolicyWrapper(this, getLoadBalancingPolicies()); + } + + protected ControlConnection buildControlConnection() { + return new ControlConnection(this); + } + + protected RequestProcessorRegistry buildRequestProcessorRegistry() { + return RequestProcessorRegistry.defaultCqlProcessors(getSessionName()); + } + + protected CodecRegistry buildCodecRegistry(String logPrefix, List> codecs) { + TypeCodec[] array = new TypeCodec[codecs.size()]; + return new DefaultCodecRegistry(logPrefix, codecs.toArray(array)); + } + + protected SchemaQueriesFactory buildSchemaQueriesFactory() { + return new DefaultSchemaQueriesFactory(this); + } + + protected SchemaParserFactory buildSchemaParserFactory() { + return new DefaultSchemaParserFactory(this); + } + + protected TokenFactoryRegistry buildTokenFactoryRegistry() { + return new DefaultTokenFactoryRegistry(this); + } + + protected ReplicationStrategyFactory buildReplicationStrategyFactory() { + return new DefaultReplicationStrategyFactory(this); + } + + protected PoolManager buildPoolManager() { + return new PoolManager(this); + } + + protected MetricsFactory buildMetricsFactory() { + return new DropwizardMetricsFactory(this); + } + + protected RequestThrottler buildRequestThrottler() { + return Reflection.buildFromConfig( + this, + DefaultDriverOption.REQUEST_THROTTLER_CLASS, + RequestThrottler.class, + "com.datastax.oss.driver.internal.core.session.throttling") + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Missing request throttler, check your configuration (%s)", + DefaultDriverOption.REQUEST_THROTTLER_CLASS))); + } + + protected NodeStateListener buildNodeStateListener( + NodeStateListener nodeStateListenerFromBuilder) { + return (nodeStateListenerFromBuilder != null) + ? nodeStateListenerFromBuilder + : Reflection.buildFromConfig( + this, + DefaultDriverOption.METADATA_NODE_STATE_LISTENER_CLASS, + NodeStateListener.class, + "com.datastax.oss.driver.internal.core.metadata") + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Missing node state listener, check your configuration (%s)", + DefaultDriverOption.METADATA_NODE_STATE_LISTENER_CLASS))); + } + + protected SchemaChangeListener buildSchemaChangeListener( + SchemaChangeListener schemaChangeListenerFromBuilder) { + return (schemaChangeListenerFromBuilder != null) + ? schemaChangeListenerFromBuilder + : Reflection.buildFromConfig( + this, + DefaultDriverOption.METADATA_SCHEMA_CHANGE_LISTENER_CLASS, + SchemaChangeListener.class, + "com.datastax.oss.driver.internal.core.metadata.schema") + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Missing schema change listener, check your configuration (%s)", + DefaultDriverOption.METADATA_SCHEMA_CHANGE_LISTENER_CLASS))); + } + + protected RequestTracker buildRequestTracker(RequestTracker requestTrackerFromBuilder) { + return (requestTrackerFromBuilder != null) + ? requestTrackerFromBuilder + : Reflection.buildFromConfig( + this, + DefaultDriverOption.REQUEST_TRACKER_CLASS, + RequestTracker.class, + "com.datastax.oss.driver.internal.core.tracker") + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Missing request tracker, check your configuration (%s)", + DefaultDriverOption.REQUEST_TRACKER_CLASS))); + } + + @NonNull + @Override + public String getSessionName() { + return sessionName; + } + + @NonNull + @Override + public DriverConfig getConfig() { + return config; + } + + @NonNull + @Override + public DriverConfigLoader getConfigLoader() { + return configLoader; + } + + @NonNull + @Override + public Map getLoadBalancingPolicies() { + return loadBalancingPoliciesRef.get(); + } + + @NonNull + @Override + public Map getRetryPolicies() { + return retryPoliciesRef.get(); + } + + @NonNull + @Override + public Map getSpeculativeExecutionPolicies() { + return speculativeExecutionPoliciesRef.get(); + } + + @NonNull + @Override + public TimestampGenerator getTimestampGenerator() { + return timestampGeneratorRef.get(); + } + + @NonNull + @Override + public ReconnectionPolicy getReconnectionPolicy() { + return reconnectionPolicyRef.get(); + } + + @NonNull + @Override + public AddressTranslator getAddressTranslator() { + return addressTranslatorRef.get(); + } + + @NonNull + @Override + public Optional getAuthProvider() { + return authProviderRef.get(); + } + + @NonNull + @Override + public Optional getSslEngineFactory() { + return sslEngineFactoryRef.get(); + } + + @NonNull + @Override + public EventBus getEventBus() { + return eventBusRef.get(); + } + + @NonNull + @Override + public Compressor getCompressor() { + return compressorRef.get(); + } + + @NonNull + @Override + public FrameCodec getFrameCodec() { + return frameCodecRef.get(); + } + + @NonNull + @Override + public ProtocolVersionRegistry getProtocolVersionRegistry() { + return protocolVersionRegistryRef.get(); + } + + @NonNull + @Override + public ConsistencyLevelRegistry getConsistencyLevelRegistry() { + return consistencyLevelRegistryRef.get(); + } + + @NonNull + @Override + public WriteTypeRegistry getWriteTypeRegistry() { + return writeTypeRegistryRef.get(); + } + + @NonNull + @Override + public NettyOptions getNettyOptions() { + return nettyOptionsRef.get(); + } + + @NonNull + @Override + public WriteCoalescer getWriteCoalescer() { + return writeCoalescerRef.get(); + } + + @NonNull + @Override + public Optional getSslHandlerFactory() { + return sslHandlerFactoryRef.get(); + } + + @NonNull + @Override + public ChannelFactory getChannelFactory() { + return channelFactoryRef.get(); + } + + @NonNull + @Override + public ChannelPoolFactory getChannelPoolFactory() { + return channelPoolFactory; + } + + @NonNull + @Override + public TopologyMonitor getTopologyMonitor() { + return topologyMonitorRef.get(); + } + + @NonNull + @Override + public MetadataManager getMetadataManager() { + return metadataManagerRef.get(); + } + + @NonNull + @Override + public LoadBalancingPolicyWrapper getLoadBalancingPolicyWrapper() { + return loadBalancingPolicyWrapperRef.get(); + } + + @NonNull + @Override + public ControlConnection getControlConnection() { + return controlConnectionRef.get(); + } + + @NonNull + @Override + public RequestProcessorRegistry getRequestProcessorRegistry() { + return requestProcessorRegistryRef.get(); + } + + @NonNull + @Override + public SchemaQueriesFactory getSchemaQueriesFactory() { + return schemaQueriesFactoryRef.get(); + } + + @NonNull + @Override + public SchemaParserFactory getSchemaParserFactory() { + return schemaParserFactoryRef.get(); + } + + @NonNull + @Override + public TokenFactoryRegistry getTokenFactoryRegistry() { + return tokenFactoryRegistryRef.get(); + } + + @NonNull + @Override + public ReplicationStrategyFactory getReplicationStrategyFactory() { + return replicationStrategyFactoryRef.get(); + } + + @NonNull + @Override + public PoolManager getPoolManager() { + return poolManagerRef.get(); + } + + @NonNull + @Override + public MetricsFactory getMetricsFactory() { + return metricsFactoryRef.get(); + } + + @NonNull + @Override + public RequestThrottler getRequestThrottler() { + return requestThrottlerRef.get(); + } + + @NonNull + @Override + public NodeStateListener getNodeStateListener() { + return nodeStateListenerRef.get(); + } + + @NonNull + @Override + public SchemaChangeListener getSchemaChangeListener() { + return schemaChangeListenerRef.get(); + } + + @NonNull + @Override + public RequestTracker getRequestTracker() { + return requestTrackerRef.get(); + } + + @Nullable + @Override + public String getLocalDatacenter(@NonNull String profileName) { + return localDatacentersFromBuilder.get(profileName); + } + + @Nullable + @Override + public Predicate getNodeFilter(@NonNull String profileName) { + return nodeFiltersFromBuilder.get(profileName); + } + + @Nullable + @Override + public ClassLoader getClassLoader() { + return classLoader; + } + + @NonNull + @Override + public CodecRegistry getCodecRegistry() { + return codecRegistry; + } + + @NonNull + @Override + public ProtocolVersion getProtocolVersion() { + return getChannelFactory().getProtocolVersion(); + } + + @NonNull + @Override + public Map getStartupOptions() { + return startupOptionsRef.get(); + } + + protected RequestLogFormatter buildRequestLogFormatter() { + return new RequestLogFormatter(this); + } + + @NonNull + @Override + public RequestLogFormatter getRequestLogFormatter() { + return requestLogFormatterRef.get(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultNettyOptions.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultNettyOptions.java new file mode 100644 index 00000000000..76b707234dc --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/DefaultNettyOptions.java @@ -0,0 +1,143 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.context; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.ThreadFactoryBuilder; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timer; +import io.netty.util.concurrent.DefaultPromise; +import io.netty.util.concurrent.EventExecutorGroup; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.concurrent.PromiseCombiner; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultNettyOptions implements NettyOptions { + private final EventLoopGroup ioEventLoopGroup; + private final EventLoopGroup adminEventLoopGroup; + private final int ioShutdownQuietPeriod; + private final int ioShutdownTimeout; + private final TimeUnit ioShutdownUnit; + private final int adminShutdownQuietPeriod; + private final int adminShutdownTimeout; + private final TimeUnit adminShutdownUnit; + private final Timer timer; + + public DefaultNettyOptions(InternalDriverContext context) { + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + int ioGroupSize = config.getInt(DefaultDriverOption.NETTY_IO_SIZE); + this.ioShutdownQuietPeriod = config.getInt(DefaultDriverOption.NETTY_IO_SHUTDOWN_QUIET_PERIOD); + this.ioShutdownTimeout = config.getInt(DefaultDriverOption.NETTY_IO_SHUTDOWN_TIMEOUT); + this.ioShutdownUnit = + TimeUnit.valueOf(config.getString(DefaultDriverOption.NETTY_IO_SHUTDOWN_UNIT)); + int adminGroupSize = config.getInt(DefaultDriverOption.NETTY_ADMIN_SIZE); + this.adminShutdownQuietPeriod = + config.getInt(DefaultDriverOption.NETTY_ADMIN_SHUTDOWN_QUIET_PERIOD); + this.adminShutdownTimeout = config.getInt(DefaultDriverOption.NETTY_ADMIN_SHUTDOWN_TIMEOUT); + this.adminShutdownUnit = + TimeUnit.valueOf(config.getString(DefaultDriverOption.NETTY_ADMIN_SHUTDOWN_UNIT)); + + ThreadFactory safeFactory = new BlockingOperation.SafeThreadFactory(); + ThreadFactory ioThreadFactory = + new ThreadFactoryBuilder() + .setThreadFactory(safeFactory) + .setNameFormat(context.getSessionName() + "-io-%d") + .build(); + this.ioEventLoopGroup = new NioEventLoopGroup(ioGroupSize, ioThreadFactory); + + ThreadFactory adminThreadFactory = + new ThreadFactoryBuilder() + .setThreadFactory(safeFactory) + .setNameFormat(context.getSessionName() + "-admin-%d") + .build(); + this.adminEventLoopGroup = new DefaultEventLoopGroup(adminGroupSize, adminThreadFactory); + // setup the Timer + ThreadFactory timerThreadFactory = + new ThreadFactoryBuilder() + .setThreadFactory(safeFactory) + .setNameFormat(context.getSessionName() + "-timer-%d") + .build(); + timer = + new HashedWheelTimer( + timerThreadFactory, + config.getDuration(DefaultDriverOption.NETTY_TIMER_TICK_DURATION).toNanos(), + TimeUnit.NANOSECONDS, + config.getInt(DefaultDriverOption.NETTY_TIMER_TICKS_PER_WHEEL)); + } + + @Override + public EventLoopGroup ioEventLoopGroup() { + return ioEventLoopGroup; + } + + @Override + public EventExecutorGroup adminEventExecutorGroup() { + return adminEventLoopGroup; + } + + @Override + public Class channelClass() { + return NioSocketChannel.class; + } + + @Override + public ByteBufAllocator allocator() { + return ByteBufAllocator.DEFAULT; + } + + @Override + public void afterBootstrapInitialized(Bootstrap bootstrap) { + // nothing to do + } + + @Override + public void afterChannelInitialized(Channel channel) { + // nothing to do + } + + @Override + public Future onClose() { + PromiseCombiner combiner = new PromiseCombiner(); + combiner.add( + adminEventLoopGroup.shutdownGracefully( + adminShutdownQuietPeriod, adminShutdownTimeout, adminShutdownUnit)); + combiner.add( + ioEventLoopGroup.shutdownGracefully( + ioShutdownQuietPeriod, ioShutdownTimeout, ioShutdownUnit)); + DefaultPromise closeFuture = new DefaultPromise<>(GlobalEventExecutor.INSTANCE); + combiner.finish(closeFuture); + closeFuture.addListener(f -> timer.stop()); + return closeFuture; + } + + @Override + public synchronized Timer getTimer() { + return timer; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/EventBus.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/EventBus.java new file mode 100644 index 00000000000..b61e1cf8149 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/EventBus.java @@ -0,0 +1,98 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.context; + +import com.datastax.oss.driver.shaded.guava.common.collect.HashMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.Multimaps; +import com.datastax.oss.driver.shaded.guava.common.collect.SetMultimap; +import java.util.function.Consumer; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Barebones event bus implementation, that allows components to communicate without knowing about + * each other. + * + *

This is intended for administrative events (topology changes, new connections, etc.), which + * are comparatively rare in the driver. Do not use it for anything on the request path, because it + * relies on synchronization. + * + *

We don't use Guava's implementation because Guava is shaded in the driver, and the event bus + * needs to be accessible from low-level 3rd party customizations. + */ +@ThreadSafe +public class EventBus { + private static final Logger LOG = LoggerFactory.getLogger(EventBus.class); + + private final String logPrefix; + private final SetMultimap, Consumer> listeners = + Multimaps.synchronizedSetMultimap(HashMultimap.create()); + + public EventBus(String logPrefix) { + this.logPrefix = logPrefix; + } + + /** + * Registers a listener for an event type. + * + *

If the listener has a shorter lifecycle than the {@code Cluster} instance, it is recommended + * to save the key returned by this method, and use it later to unregister and therefore avoid a + * leak. + * + * @return a key that is needed to unregister later. + */ + public Object register(Class eventClass, Consumer listener) { + LOG.debug("[{}] Registering {} for {}", logPrefix, listener, eventClass); + listeners.put(eventClass, listener); + // The reason for the key mechanism is that this will often be used with method references, + // and you get a different object every time you reference a method, so register(Foo::bar) + // followed by unregister(Foo::bar) wouldn't work as expected. + return listener; + } + + /** + * Unregisters a listener. + * + * @param key the key that was returned by {@link #register(Class, Consumer)} + */ + public boolean unregister(Object key, Class eventClass) { + LOG.debug("[{}] Unregistering {} for {}", logPrefix, key, eventClass); + return listeners.remove(eventClass, key); + } + + /** + * Sends an event that will notify any registered listener for that class. + * + *

Listeners are looked up by an exact match on the class of the object, as returned by + * {@code event.getClass()}. Listeners of a supertype won't be notified. + * + *

The listeners are invoked on the calling thread. It's their responsibility to schedule event + * processing asynchronously if needed. + */ + public void fire(Object event) { + LOG.debug("[{}] Firing an instance of {}: {}", logPrefix, event.getClass(), event); + // if the exact match thing gets too cumbersome, we can reconsider, but I'd like to avoid + // scanning all the keys with instanceof checks. + Class eventClass = event.getClass(); + for (Consumer l : listeners.get(eventClass)) { + @SuppressWarnings("unchecked") + Consumer listener = (Consumer) l; + LOG.debug("[{}] Notifying {} of {}", logPrefix, listener, event); + listener.accept(event); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/InternalDriverContext.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/InternalDriverContext.java new file mode 100644 index 00000000000..afc5dbce92e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/InternalDriverContext.java @@ -0,0 +1,174 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.context; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import com.datastax.oss.driver.internal.core.ConsistencyLevelRegistry; +import com.datastax.oss.driver.internal.core.ProtocolVersionRegistry; +import com.datastax.oss.driver.internal.core.channel.ChannelFactory; +import com.datastax.oss.driver.internal.core.channel.WriteCoalescer; +import com.datastax.oss.driver.internal.core.control.ControlConnection; +import com.datastax.oss.driver.internal.core.metadata.LoadBalancingPolicyWrapper; +import com.datastax.oss.driver.internal.core.metadata.MetadataManager; +import com.datastax.oss.driver.internal.core.metadata.TopologyMonitor; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.SchemaParserFactory; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaQueriesFactory; +import com.datastax.oss.driver.internal.core.metadata.token.ReplicationStrategyFactory; +import com.datastax.oss.driver.internal.core.metadata.token.TokenFactoryRegistry; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.internal.core.pool.ChannelPoolFactory; +import com.datastax.oss.driver.internal.core.servererrors.WriteTypeRegistry; +import com.datastax.oss.driver.internal.core.session.PoolManager; +import com.datastax.oss.driver.internal.core.session.RequestProcessorRegistry; +import com.datastax.oss.driver.internal.core.ssl.SslHandlerFactory; +import com.datastax.oss.driver.internal.core.tracker.RequestLogFormatter; +import com.datastax.oss.protocol.internal.Compressor; +import com.datastax.oss.protocol.internal.FrameCodec; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import io.netty.buffer.ByteBuf; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; + +/** Extends the driver context with additional components that are not exposed by our public API. */ +public interface InternalDriverContext extends DriverContext { + + @NonNull + EventBus getEventBus(); + + @NonNull + Compressor getCompressor(); + + @NonNull + FrameCodec getFrameCodec(); + + @NonNull + ProtocolVersionRegistry getProtocolVersionRegistry(); + + @NonNull + ConsistencyLevelRegistry getConsistencyLevelRegistry(); + + @NonNull + WriteTypeRegistry getWriteTypeRegistry(); + + @NonNull + NettyOptions getNettyOptions(); + + @NonNull + WriteCoalescer getWriteCoalescer(); + + @NonNull + Optional getSslHandlerFactory(); + + @NonNull + ChannelFactory getChannelFactory(); + + @NonNull + ChannelPoolFactory getChannelPoolFactory(); + + @NonNull + TopologyMonitor getTopologyMonitor(); + + @NonNull + MetadataManager getMetadataManager(); + + @NonNull + LoadBalancingPolicyWrapper getLoadBalancingPolicyWrapper(); + + @NonNull + ControlConnection getControlConnection(); + + @NonNull + RequestProcessorRegistry getRequestProcessorRegistry(); + + @NonNull + SchemaQueriesFactory getSchemaQueriesFactory(); + + @NonNull + SchemaParserFactory getSchemaParserFactory(); + + @NonNull + TokenFactoryRegistry getTokenFactoryRegistry(); + + @NonNull + ReplicationStrategyFactory getReplicationStrategyFactory(); + + @NonNull + PoolManager getPoolManager(); + + @NonNull + MetricsFactory getMetricsFactory(); + + /** + * The value that was passed to {@link SessionBuilder#withLocalDatacenter(String,String)} for this + * particular profile. If it was specified through the configuration instead, this method will + * return {@code null}. + */ + @Nullable + String getLocalDatacenter(@NonNull String profileName); + + /** + * This is the filter from {@link SessionBuilder#withNodeFilter(String, Predicate)}. If the filter + * for this profile was specified through the configuration instead, this method will return + * {@code null}. + */ + @Nullable + Predicate getNodeFilter(@NonNull String profileName); + + /** + * The {@link ClassLoader} to use to reflectively load class names defined in configuration. If + * null, the driver attempts to use {@link Thread#getContextClassLoader()} of the current thread + * or {@link com.datastax.oss.driver.internal.core.util.Reflection}'s {@link ClassLoader}. + */ + @Nullable + ClassLoader getClassLoader(); + + /** + * Retrieves the map of options to send in a Startup message. The returned map will be used to + * construct a {@link com.datastax.oss.protocol.internal.request.Startup} instance when + * initializing the native protocol handshake. + */ + @NonNull + Map getStartupOptions(); + + /** + * A list of additional components to notify of session lifecycle events. + * + *

The default implementation returns an empty list. Custom driver extensions might override + * this method to add their own components. + * + *

Note that the driver assumes that the returned list is constant; there is no way to add + * listeners dynamically. + */ + @NonNull + default List getLifecycleListeners() { + return Collections.emptyList(); + } + + /** + * A {@link RequestLogFormatter} instance based on this {@link DriverContext}. + * + *

The {@link RequestLogFormatter} instance returned here will use the settings in + * advanced.request-tracker when formatting requests. + */ + @NonNull + RequestLogFormatter getRequestLogFormatter(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/LifecycleListener.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/LifecycleListener.java new file mode 100644 index 00000000000..31fcacfdcf1 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/LifecycleListener.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.context; + +import com.datastax.oss.driver.api.core.session.SessionBuilder; + +/** + * A component that gets notified of certain events in the session's lifecycle. + * + *

This is intended for third-party extensions, no built-in components implement this. + */ +public interface LifecycleListener extends AutoCloseable { + + /** + * Invoked when the session is ready to process user requests. + * + *

This corresponds to the moment when the {@link SessionBuilder#build()} returns, or the + * future returned by {@link SessionBuilder#buildAsync()} completes. If the session initialization + * fails, this method will not get called. + * + *

This method is invoked on a driver thread, it should complete relatively quickly and not + * block. + */ + void onSessionReady(); + + /** + * Invoked when the session shuts down. + * + *

Implementations should perform any necessary cleanup, for example freeing resources or + * cancelling scheduled tasks. + * + *

Note that this method gets called even if the shutdown results from a failed initialization. + * In that case, implementations should be ready to handle a call to this method even though + * {@link #onSessionReady()} hasn't been invoked. + * + *

This method is invoked on a driver thread, it should complete relatively quickly and not + * block. + */ + @Override + void close() throws Exception; +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/NettyOptions.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/NettyOptions.java new file mode 100644 index 00000000000..12f8506883a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/NettyOptions.java @@ -0,0 +1,92 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.context; + +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.EventLoopGroup; +import io.netty.util.Timer; +import io.netty.util.concurrent.EventExecutorGroup; +import io.netty.util.concurrent.Future; + +/** Low-level hooks to control certain aspects of Netty usage in the driver. */ +public interface NettyOptions { + + /** + * The event loop group that will be used for I/O. This must always return the same instance. + * + *

It is highly recommended that the threads in this event loop group be created by a {@link + * BlockingOperation.SafeThreadFactory}, so that the driver can protect against deadlocks + * introduced by bad client code. + */ + EventLoopGroup ioEventLoopGroup(); + + /** + * The class to create {@code Channel} instances from. This must be consistent with {@link + * #ioEventLoopGroup()}. + */ + Class channelClass(); + + /** + * An event executor group that will be used to schedule all tasks not related to request I/O: + * cluster events, refreshing metadata, reconnection, etc. + * + *

This must always return the same instance (it can be the same object as {@link + * #ioEventLoopGroup()}). + * + *

It is highly recommended that the threads in this event loop group be created by a {@link + * BlockingOperation.SafeThreadFactory}, so that the driver can protect against deadlocks + * introduced by bad client code. + */ + EventExecutorGroup adminEventExecutorGroup(); + + /** + * The byte buffer allocator to use. This must always return the same instance. Note that this is + * also used by the default implementation of {@link InternalDriverContext#getFrameCodec()}, and + * the built-in {@link com.datastax.oss.protocol.internal.Compressor} implementations. + */ + ByteBufAllocator allocator(); + + /** + * A hook invoked each time the driver creates a client bootstrap in order to open a channel. This + * is a good place to configure any custom option on the bootstrap. + */ + void afterBootstrapInitialized(Bootstrap bootstrap); + + /** + * A hook invoked on each channel, right after the channel has initialized it. This is a good + * place to register any custom handler on the channel's pipeline (note that built-in driver + * handlers are already installed at that point). + */ + void afterChannelInitialized(Channel channel); + + /** + * A hook involved when the driver instance shuts down. This is a good place to free any resources + * that you have allocated elsewhere in this component, for example shut down custom event loop + * groups. + */ + Future onClose(); + + /** + * The Timer on which non-I/O events should be scheduled. This must always return the same + * instance. This timer should be used for things like request timeout events and scheduling + * speculative executions. Under high load, scheduling these non-I/O events on a separate, lower + * resolution timer will allow for higher overall I/O throughput. + */ + Timer getTimer(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/context/StartupOptionsBuilder.java b/core/src/main/java/com/datastax/oss/driver/internal/core/context/StartupOptionsBuilder.java new file mode 100644 index 00000000000..49718b7df97 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/context/StartupOptionsBuilder.java @@ -0,0 +1,80 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.context; + +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.protocol.internal.request.Startup; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableMap; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class StartupOptionsBuilder { + + public static final String DRIVER_NAME_KEY = "DRIVER_NAME"; + public static final String DRIVER_VERSION_KEY = "DRIVER_VERSION"; + + protected final InternalDriverContext context; + + public StartupOptionsBuilder(InternalDriverContext context) { + this.context = context; + } + + /** + * Builds a map of options to send in a Startup message. + * + *

The default set of options are built here and include {@link + * com.datastax.oss.protocol.internal.request.Startup#COMPRESSION_KEY} (if the context passed in + * has a compressor/algorithm set), and the driver's {@link #DRIVER_NAME_KEY} and {@link + * #DRIVER_VERSION_KEY}. The {@link com.datastax.oss.protocol.internal.request.Startup} + * constructor will add {@link + * com.datastax.oss.protocol.internal.request.Startup#CQL_VERSION_KEY}. + * + * @return Map of Startup Options. + */ + public Map build() { + NullAllowingImmutableMap.Builder builder = NullAllowingImmutableMap.builder(3); + // add compression (if configured) and driver name and version + String compressionAlgorithm = context.getCompressor().algorithm(); + if (compressionAlgorithm != null && !compressionAlgorithm.trim().isEmpty()) { + builder.put(Startup.COMPRESSION_KEY, compressionAlgorithm.trim()); + } + return builder + .put(DRIVER_NAME_KEY, getDriverName()) + .put(DRIVER_VERSION_KEY, getDriverVersion()) + .build(); + } + + /** + * Returns this driver's name. + * + *

By default, this method will pull from the bundled Driver.properties file. Subclasses should + * override this method if they need to report a different Driver name on Startup. + */ + protected String getDriverName() { + return Session.OSS_DRIVER_COORDINATES.getName(); + } + + /** + * Returns this driver's version. + * + *

By default, this method will pull from the bundled Driver.properties file. Subclasses should + * override this method if they need to report a different Driver version on Startup. + */ + protected String getDriverVersion() { + return Session.OSS_DRIVER_COORDINATES.getVersion().toString(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/control/ControlConnection.java b/core/src/main/java/com/datastax/oss/driver/internal/core/control/ControlConnection.java new file mode 100644 index 00000000000..50b6ffe90f0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/control/ControlConnection.java @@ -0,0 +1,579 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.control; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.AsyncAutoCloseable; +import com.datastax.oss.driver.api.core.auth.AuthenticationException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.internal.core.channel.ChannelEvent; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.DriverChannelOptions; +import com.datastax.oss.driver.internal.core.channel.EventCallback; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultTopologyMonitor; +import com.datastax.oss.driver.internal.core.metadata.DistanceEvent; +import com.datastax.oss.driver.internal.core.metadata.MetadataManager; +import com.datastax.oss.driver.internal.core.metadata.NodeStateEvent; +import com.datastax.oss.driver.internal.core.metadata.TopologyEvent; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.internal.core.util.concurrent.Reconnection; +import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule; +import com.datastax.oss.driver.internal.core.util.concurrent.UncaughtExceptions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.Event; +import com.datastax.oss.protocol.internal.response.event.SchemaChangeEvent; +import com.datastax.oss.protocol.internal.response.event.StatusChangeEvent; +import com.datastax.oss.protocol.internal.response.event.TopologyChangeEvent; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.util.concurrent.EventExecutor; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Queue; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains a dedicated connection to a Cassandra node for administrative queries. + * + *

If the control node goes down, a reconnection is triggered. The control node is chosen + * randomly among the contact points at startup, or according to the load balancing policy for later + * reconnections. + * + *

The control connection is used by: + * + *

    + *
  • {@link DefaultTopologyMonitor} to determine cluster connectivity and retrieve node + * metadata; + *
  • {@link MetadataManager} to run schema metadata queries. + *
+ */ +@ThreadSafe +public class ControlConnection implements EventCallback, AsyncAutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(ControlConnection.class); + + private final InternalDriverContext context; + private final String logPrefix; + private final EventExecutor adminExecutor; + private final SingleThreaded singleThreaded; + + // The single channel used by this connection. This field is accessed concurrently, but only + // mutated on adminExecutor (by SingleThreaded methods) + private volatile DriverChannel channel; + + public ControlConnection(InternalDriverContext context) { + this.context = context; + this.logPrefix = context.getSessionName(); + this.adminExecutor = context.getNettyOptions().adminEventExecutorGroup().next(); + this.singleThreaded = new SingleThreaded(context); + } + + /** + * Initializes the control connection. If it is already initialized, this is a no-op and all + * parameters are ignored. + * + * @param listenToClusterEvents whether to register for TOPOLOGY_CHANGE and STATUS_CHANGE events. + * If the control connection has already initialized with another value, this is ignored. + * SCHEMA_CHANGE events are always registered. + * @param reconnectOnFailure whether to schedule a reconnection if the initial attempt fails (this + * does not affect the returned future, which always represent the outcome of the initial + * attempt only). + * @param useInitialReconnectionSchedule if no node can be reached, the type of reconnection + * schedule to use. In other words, the value that will be passed to {@link + * ReconnectionPolicy#newControlConnectionSchedule(boolean)}. Note that this parameter is only + * relevant if {@code reconnectOnFailure} is true, otherwise it is not used. + */ + public CompletionStage init( + boolean listenToClusterEvents, + boolean reconnectOnFailure, + boolean useInitialReconnectionSchedule) { + RunOrSchedule.on( + adminExecutor, + () -> + singleThreaded.init( + listenToClusterEvents, reconnectOnFailure, useInitialReconnectionSchedule)); + return singleThreaded.initFuture; + } + + public CompletionStage initFuture() { + return singleThreaded.initFuture; + } + + public boolean isInit() { + return singleThreaded.initFuture.isDone(); + } + + public CompletionStage firstConnectionAttemptFuture() { + return singleThreaded.firstConnectionAttemptFuture; + } + + /** + * The channel currently used by this control connection. This is modified concurrently in the + * event of a reconnection, so it may occasionally return a closed channel (clients should be + * ready to deal with that). + */ + public DriverChannel channel() { + return channel; + } + + /** + * Forces an immediate reconnect: if we were connected to a node, that connection will be closed; + * if we were already reconnecting, the next attempt is started immediately, without waiting for + * the next scheduled interval; in all cases, a new query plan is fetched from the load balancing + * policy, and each node in it will be tried in sequence. + */ + public void reconnectNow() { + RunOrSchedule.on(adminExecutor, singleThreaded::reconnectNow); + } + + @NonNull + @Override + public CompletionStage closeFuture() { + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage closeAsync() { + // Control queries are never critical, so there is no graceful close. + return forceCloseAsync(); + } + + @NonNull + @Override + public CompletionStage forceCloseAsync() { + RunOrSchedule.on(adminExecutor, singleThreaded::forceClose); + return singleThreaded.closeFuture; + } + + @Override + public void onEvent(Message eventMessage) { + if (!(eventMessage instanceof Event)) { + LOG.warn("[{}] Unsupported event class: {}", logPrefix, eventMessage.getClass().getName()); + } else { + LOG.debug("[{}] Processing incoming event {}", logPrefix, eventMessage); + Event event = (Event) eventMessage; + switch (event.type) { + case ProtocolConstants.EventType.TOPOLOGY_CHANGE: + processTopologyChange(event); + break; + case ProtocolConstants.EventType.STATUS_CHANGE: + processStatusChange(event); + break; + case ProtocolConstants.EventType.SCHEMA_CHANGE: + processSchemaChange(event); + break; + default: + LOG.warn("[{}] Unsupported event type: {}", logPrefix, event.type); + } + } + } + + private void processTopologyChange(Event event) { + TopologyChangeEvent tce = (TopologyChangeEvent) event; + switch (tce.changeType) { + case ProtocolConstants.TopologyChangeType.NEW_NODE: + context.getEventBus().fire(TopologyEvent.suggestAdded(tce.address)); + break; + case ProtocolConstants.TopologyChangeType.REMOVED_NODE: + context.getEventBus().fire(TopologyEvent.suggestRemoved(tce.address)); + break; + default: + LOG.warn("[{}] Unsupported topology change type: {}", logPrefix, tce.changeType); + } + } + + private void processStatusChange(Event event) { + StatusChangeEvent sce = (StatusChangeEvent) event; + switch (sce.changeType) { + case ProtocolConstants.StatusChangeType.UP: + context.getEventBus().fire(TopologyEvent.suggestUp(sce.address)); + break; + case ProtocolConstants.StatusChangeType.DOWN: + context.getEventBus().fire(TopologyEvent.suggestDown(sce.address)); + break; + default: + LOG.warn("[{}] Unsupported status change type: {}", logPrefix, sce.changeType); + } + } + + private void processSchemaChange(Event event) { + SchemaChangeEvent sce = (SchemaChangeEvent) event; + context.getMetadataManager().refreshSchema(sce.keyspace, false, false); + } + + private class SingleThreaded { + private final InternalDriverContext context; + private final DriverConfig config; + private final CompletableFuture initFuture = new CompletableFuture<>(); + private final CompletableFuture firstConnectionAttemptFuture = new CompletableFuture<>(); + private boolean initWasCalled; + private final CompletableFuture closeFuture = new CompletableFuture<>(); + private boolean closeWasCalled; + private final ReconnectionPolicy reconnectionPolicy; + private final Reconnection reconnection; + private DriverChannelOptions channelOptions; + // The last events received for each node + private final Map lastDistanceEvents = new WeakHashMap<>(); + private final Map lastStateEvents = new WeakHashMap<>(); + + private SingleThreaded(InternalDriverContext context) { + this.context = context; + this.config = context.getConfig(); + this.reconnectionPolicy = context.getReconnectionPolicy(); + this.reconnection = + new Reconnection( + logPrefix, + adminExecutor, + () -> reconnectionPolicy.newControlConnectionSchedule(false), + this::reconnect); + // In "reconnect-on-init" mode, handle cancellation of the initFuture by user code + CompletableFutures.whenCancelled( + this.initFuture, + () -> { + LOG.debug("[{}] Init future was cancelled, stopping reconnection", logPrefix); + reconnection.stop(); + }); + + context + .getEventBus() + .register(DistanceEvent.class, RunOrSchedule.on(adminExecutor, this::onDistanceEvent)); + context + .getEventBus() + .register(NodeStateEvent.class, RunOrSchedule.on(adminExecutor, this::onStateEvent)); + } + + private void init( + boolean listenToClusterEvents, + boolean reconnectOnFailure, + boolean useInitialReconnectionSchedule) { + assert adminExecutor.inEventLoop(); + if (initWasCalled) { + return; + } + initWasCalled = true; + try { + ImmutableList eventTypes = buildEventTypes(listenToClusterEvents); + LOG.debug("[{}] Initializing with event types {}", logPrefix, eventTypes); + channelOptions = + DriverChannelOptions.builder() + .withEvents(eventTypes, ControlConnection.this) + .withOwnerLogPrefix(logPrefix + "|control") + .build(); + + Queue nodes = context.getLoadBalancingPolicyWrapper().newQueryPlan(); + + connect( + nodes, + null, + () -> { + initFuture.complete(null); + firstConnectionAttemptFuture.complete(null); + }, + error -> { + if (isAuthFailure(error)) { + LOG.warn( + "[{}] Authentication errors encountered on all contact points. Please check your authentication configuration.", + logPrefix); + } + if (reconnectOnFailure && !closeWasCalled) { + reconnection.start( + reconnectionPolicy.newControlConnectionSchedule( + useInitialReconnectionSchedule)); + } else { + // Special case for the initial connection: reword to a more user-friendly error + // message + if (error instanceof AllNodesFailedException) { + error = + ((AllNodesFailedException) error) + .reword( + "Could not reach any contact point, " + + "make sure you've provided valid addresses"); + } + initFuture.completeExceptionally(error); + } + firstConnectionAttemptFuture.completeExceptionally(error); + }); + } catch (Throwable t) { + initFuture.completeExceptionally(t); + } + } + + private CompletionStage reconnect() { + assert adminExecutor.inEventLoop(); + Queue nodes = context.getLoadBalancingPolicyWrapper().newQueryPlan(); + CompletableFuture result = new CompletableFuture<>(); + connect( + nodes, + null, + () -> { + result.complete(true); + onSuccessfulReconnect(); + }, + error -> result.complete(false)); + return result; + } + + private void connect( + Queue nodes, + Map errors, + Runnable onSuccess, + Consumer onFailure) { + assert adminExecutor.inEventLoop(); + Node node = nodes.poll(); + if (node == null) { + onFailure.accept(AllNodesFailedException.fromErrors(errors)); + } else { + LOG.debug("[{}] Trying to establish a connection to {}", logPrefix, node); + context + .getChannelFactory() + .connect(node, channelOptions) + .whenCompleteAsync( + (channel, error) -> { + try { + DistanceEvent lastDistanceEvent = lastDistanceEvents.get(node); + NodeStateEvent lastStateEvent = lastStateEvents.get(node); + if (error != null) { + if (closeWasCalled || initFuture.isCancelled()) { + onSuccess.run(); // abort, we don't really care about the result + } else { + if (error instanceof AuthenticationException) { + Loggers.warnWithException( + LOG, "[{}] Authentication error", logPrefix, error); + } else { + if (config + .getDefaultProfile() + .getBoolean(DefaultDriverOption.CONNECTION_WARN_INIT_ERROR)) { + Loggers.warnWithException( + LOG, + "[{}] Error connecting to {}, trying next node", + logPrefix, + node, + error); + } else { + LOG.debug( + "[{}] Error connecting to {}, trying next node", + logPrefix, + node, + error); + } + } + Map newErrors = + (errors == null) ? new LinkedHashMap<>() : errors; + newErrors.put(node, error); + context.getEventBus().fire(ChannelEvent.controlConnectionFailed(node)); + connect(nodes, newErrors, onSuccess, onFailure); + } + } else if (closeWasCalled || initFuture.isCancelled()) { + LOG.debug( + "[{}] New channel opened ({}) but the control connection was closed, closing it", + logPrefix, + channel); + channel.forceClose(); + onSuccess.run(); + } else if (lastDistanceEvent != null + && lastDistanceEvent.distance == NodeDistance.IGNORED) { + LOG.debug( + "[{}] New channel opened ({}) but node became ignored, " + + "closing and trying next node", + logPrefix, + channel); + channel.forceClose(); + connect(nodes, errors, onSuccess, onFailure); + } else if (lastStateEvent != null + && (lastStateEvent.newState == null /*(removed)*/ + || lastStateEvent.newState == NodeState.FORCED_DOWN)) { + LOG.debug( + "[{}] New channel opened ({}) but node was removed or forced down, " + + "closing and trying next node", + logPrefix, + channel); + channel.forceClose(); + connect(nodes, errors, onSuccess, onFailure); + } else { + LOG.debug("[{}] Connection established to {}", logPrefix, node); + // Make sure previous channel gets closed (it may still be open if + // reconnection was forced) + DriverChannel previousChannel = ControlConnection.this.channel; + if (previousChannel != null) { + previousChannel.forceClose(); + } + ControlConnection.this.channel = channel; + context.getEventBus().fire(ChannelEvent.channelOpened(node)); + channel + .closeFuture() + .addListener( + f -> + adminExecutor + .submit(() -> onChannelClosed(channel, node)) + .addListener(UncaughtExceptions::log)); + onSuccess.run(); + } + } catch (Exception e) { + Loggers.warnWithException( + LOG, + "[{}] Unexpected exception while processing channel init result", + logPrefix, + e); + } + }, + adminExecutor); + } + } + + private void onSuccessfulReconnect() { + // If reconnectOnFailure was true and we've never connected before, complete the future now, + // otherwise it's already complete and this is a no-op. + initFuture.complete(null); + + // Always perform a full refresh (we don't know how long we were disconnected) + context + .getMetadataManager() + .refreshNodes() + .whenComplete( + (result, error) -> { + if (error != null) { + LOG.debug("[{}] Error while refreshing node list", logPrefix, error); + } else { + try { + // A failed node list refresh at startup is not fatal, so this might be the + // first successful refresh; make sure the LBP gets initialized (this is a no-op + // if it was initialized already). + context.getLoadBalancingPolicyWrapper().init(); + context.getMetadataManager().refreshSchema(null, false, true); + } catch (Throwable t) { + Loggers.warnWithException( + LOG, "[{}] Unexpected error on control connection reconnect", logPrefix, t); + } + } + }); + } + + private void onChannelClosed(DriverChannel channel, Node node) { + assert adminExecutor.inEventLoop(); + if (!closeWasCalled) { + LOG.debug("[{}] Lost channel {}", logPrefix, channel); + context.getEventBus().fire(ChannelEvent.channelClosed(node)); + reconnection.start(); + } + } + + private void reconnectNow() { + assert adminExecutor.inEventLoop(); + if (initWasCalled && !closeWasCalled) { + reconnection.reconnectNow(true); + } + } + + private void onDistanceEvent(DistanceEvent event) { + assert adminExecutor.inEventLoop(); + this.lastDistanceEvents.put(event.node, event); + if (event.distance == NodeDistance.IGNORED + && channel != null + && !channel.closeFuture().isDone() + && event.node.getEndPoint().equals(channel.getEndPoint())) { + LOG.debug( + "[{}] Control node {} became IGNORED, reconnecting to a different node", + logPrefix, + event.node); + reconnectNow(); + } + } + + private void onStateEvent(NodeStateEvent event) { + assert adminExecutor.inEventLoop(); + this.lastStateEvents.put(event.node, event); + if ((event.newState == null /*(removed)*/ || event.newState == NodeState.FORCED_DOWN) + && channel != null + && !channel.closeFuture().isDone() + && event.node.getEndPoint().equals(channel.getEndPoint())) { + LOG.debug( + "[{}] Control node {} was removed or forced down, reconnecting to a different node", + logPrefix, + event.node); + reconnectNow(); + } + } + + private void forceClose() { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + return; + } + closeWasCalled = true; + LOG.debug("[{}] Starting shutdown", logPrefix); + reconnection.stop(); + if (channel == null) { + LOG.debug("[{}] Shutdown complete", logPrefix); + closeFuture.complete(null); + } else { + channel + .forceClose() + .addListener( + f -> { + if (f.isSuccess()) { + LOG.debug("[{}] Shutdown complete", logPrefix); + closeFuture.complete(null); + } else { + closeFuture.completeExceptionally(f.cause()); + } + }); + } + } + } + + private boolean isAuthFailure(Throwable error) { + boolean authFailure = true; + if (error instanceof AllNodesFailedException) { + Collection errors = ((AllNodesFailedException) error).getErrors().values(); + if (errors.size() == 0) { + return false; + } + for (Throwable nodeError : errors) { + if (!(nodeError instanceof AuthenticationException)) { + authFailure = false; + break; + } + } + } + return authFailure; + } + + private static ImmutableList buildEventTypes(boolean listenClusterEvents) { + ImmutableList.Builder builder = ImmutableList.builder(); + builder.add(ProtocolConstants.EventType.SCHEMA_CHANGE); + if (listenClusterEvents) { + builder + .add(ProtocolConstants.EventType.STATUS_CHANGE) + .add(ProtocolConstants.EventType.TOPOLOGY_CHANGE); + } + return builder.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/Conversions.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/Conversions.java new file mode 100644 index 00000000000..652092e0cff --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/Conversions.java @@ -0,0 +1,488 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BatchableStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.PrepareRequest; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.servererrors.AlreadyExistsException; +import com.datastax.oss.driver.api.core.servererrors.BootstrappingException; +import com.datastax.oss.driver.api.core.servererrors.CoordinatorException; +import com.datastax.oss.driver.api.core.servererrors.FunctionFailureException; +import com.datastax.oss.driver.api.core.servererrors.InvalidConfigurationInQueryException; +import com.datastax.oss.driver.api.core.servererrors.InvalidQueryException; +import com.datastax.oss.driver.api.core.servererrors.OverloadedException; +import com.datastax.oss.driver.api.core.servererrors.ProtocolError; +import com.datastax.oss.driver.api.core.servererrors.ReadFailureException; +import com.datastax.oss.driver.api.core.servererrors.ReadTimeoutException; +import com.datastax.oss.driver.api.core.servererrors.ServerError; +import com.datastax.oss.driver.api.core.servererrors.SyntaxError; +import com.datastax.oss.driver.api.core.servererrors.TruncateException; +import com.datastax.oss.driver.api.core.servererrors.UnauthorizedException; +import com.datastax.oss.driver.api.core.servererrors.UnavailableException; +import com.datastax.oss.driver.api.core.servererrors.WriteFailureException; +import com.datastax.oss.driver.api.core.servererrors.WriteTimeoutException; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.ConsistencyLevelRegistry; +import com.datastax.oss.driver.internal.core.DefaultProtocolFeature; +import com.datastax.oss.driver.internal.core.ProtocolVersionRegistry; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.token.ByteOrderedToken; +import com.datastax.oss.driver.internal.core.metadata.token.Murmur3Token; +import com.datastax.oss.driver.internal.core.metadata.token.RandomToken; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.primitives.Ints; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.request.Batch; +import com.datastax.oss.protocol.internal.request.Execute; +import com.datastax.oss.protocol.internal.request.Query; +import com.datastax.oss.protocol.internal.request.query.QueryOptions; +import com.datastax.oss.protocol.internal.response.Error; +import com.datastax.oss.protocol.internal.response.Result; +import com.datastax.oss.protocol.internal.response.error.AlreadyExists; +import com.datastax.oss.protocol.internal.response.error.ReadFailure; +import com.datastax.oss.protocol.internal.response.error.ReadTimeout; +import com.datastax.oss.protocol.internal.response.error.Unavailable; +import com.datastax.oss.protocol.internal.response.error.WriteFailure; +import com.datastax.oss.protocol.internal.response.error.WriteTimeout; +import com.datastax.oss.protocol.internal.response.result.ColumnSpec; +import com.datastax.oss.protocol.internal.response.result.Prepared; +import com.datastax.oss.protocol.internal.response.result.Rows; +import com.datastax.oss.protocol.internal.response.result.RowsMetadata; +import com.datastax.oss.protocol.internal.util.Bytes; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableList; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableMap; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Utility methods to convert to/from protocol messages. + * + *

The main goal of this class is to move this code out of the request handlers. + */ +public class Conversions { + + public static DriverExecutionProfile resolveExecutionProfile( + Request request, DriverContext context) { + if (request.getExecutionProfile() != null) { + return request.getExecutionProfile(); + } else { + DriverConfig config = context.getConfig(); + String profileName = request.getExecutionProfileName(); + return (profileName == null || profileName.isEmpty()) + ? config.getDefaultProfile() + : config.getProfile(profileName); + } + } + + public static Message toMessage( + Statement statement, DriverExecutionProfile config, InternalDriverContext context) { + ConsistencyLevelRegistry consistencyLevelRegistry = context.getConsistencyLevelRegistry(); + ConsistencyLevel consistency = statement.getConsistencyLevel(); + int consistencyCode = + (consistency == null) + ? consistencyLevelRegistry.nameToCode( + config.getString(DefaultDriverOption.REQUEST_CONSISTENCY)) + : consistency.getProtocolCode(); + int pageSize = statement.getPageSize(); + if (pageSize <= 0) { + pageSize = config.getInt(DefaultDriverOption.REQUEST_PAGE_SIZE); + } + ConsistencyLevel serialConsistency = statement.getSerialConsistencyLevel(); + int serialConsistencyCode = + (serialConsistency == null) + ? consistencyLevelRegistry.nameToCode( + config.getString(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY)) + : serialConsistency.getProtocolCode(); + long timestamp = statement.getQueryTimestamp(); + if (timestamp == Long.MIN_VALUE) { + timestamp = context.getTimestampGenerator().next(); + } + CodecRegistry codecRegistry = context.getCodecRegistry(); + ProtocolVersion protocolVersion = context.getProtocolVersion(); + ProtocolVersionRegistry protocolVersionRegistry = context.getProtocolVersionRegistry(); + CqlIdentifier keyspace = statement.getKeyspace(); + if (statement instanceof SimpleStatement) { + SimpleStatement simpleStatement = (SimpleStatement) statement; + List positionalValues = simpleStatement.getPositionalValues(); + Map namedValues = simpleStatement.getNamedValues(); + if (!positionalValues.isEmpty() && !namedValues.isEmpty()) { + throw new IllegalArgumentException( + "Can't have both positional and named values in a statement."); + } + if (keyspace != null + && !protocolVersionRegistry.supports( + protocolVersion, DefaultProtocolFeature.PER_REQUEST_KEYSPACE)) { + throw new IllegalArgumentException( + "Can't use per-request keyspace with protocol " + protocolVersion); + } + QueryOptions queryOptions = + new QueryOptions( + consistencyCode, + encode(positionalValues, codecRegistry, protocolVersion), + encode(namedValues, codecRegistry, protocolVersion), + false, + pageSize, + statement.getPagingState(), + serialConsistencyCode, + timestamp, + (keyspace == null) ? null : keyspace.asInternal()); + return new Query(simpleStatement.getQuery(), queryOptions); + } else if (statement instanceof BoundStatement) { + BoundStatement boundStatement = (BoundStatement) statement; + if (!protocolVersionRegistry.supports( + protocolVersion, DefaultProtocolFeature.UNSET_BOUND_VALUES)) { + ensureAllSet(boundStatement); + } + boolean skipMetadata = + boundStatement.getPreparedStatement().getResultSetDefinitions().size() > 0; + QueryOptions queryOptions = + new QueryOptions( + consistencyCode, + boundStatement.getValues(), + Collections.emptyMap(), + skipMetadata, + pageSize, + statement.getPagingState(), + serialConsistencyCode, + timestamp, + null); + PreparedStatement preparedStatement = boundStatement.getPreparedStatement(); + ByteBuffer id = preparedStatement.getId(); + ByteBuffer resultMetadataId = preparedStatement.getResultMetadataId(); + return new Execute( + Bytes.getArray(id), + (resultMetadataId == null) ? null : Bytes.getArray(resultMetadataId), + queryOptions); + } else if (statement instanceof BatchStatement) { + BatchStatement batchStatement = (BatchStatement) statement; + if (!protocolVersionRegistry.supports( + protocolVersion, DefaultProtocolFeature.UNSET_BOUND_VALUES)) { + ensureAllSet(batchStatement); + } + if (keyspace != null + && !protocolVersionRegistry.supports( + protocolVersion, DefaultProtocolFeature.PER_REQUEST_KEYSPACE)) { + throw new IllegalArgumentException( + "Can't use per-request keyspace with protocol " + protocolVersion); + } + List queriesOrIds = new ArrayList<>(batchStatement.size()); + List> values = new ArrayList<>(batchStatement.size()); + for (BatchableStatement child : batchStatement) { + if (child instanceof SimpleStatement) { + SimpleStatement simpleStatement = (SimpleStatement) child; + if (simpleStatement.getNamedValues().size() > 0) { + throw new IllegalArgumentException( + String.format( + "Batch statements cannot contain simple statements with named values " + + "(offending statement: %s)", + simpleStatement.getQuery())); + } + queriesOrIds.add(simpleStatement.getQuery()); + values.add(encode(simpleStatement.getPositionalValues(), codecRegistry, protocolVersion)); + } else if (child instanceof BoundStatement) { + BoundStatement boundStatement = (BoundStatement) child; + queriesOrIds.add(Bytes.getArray(boundStatement.getPreparedStatement().getId())); + values.add(boundStatement.getValues()); + } else { + throw new IllegalArgumentException( + "Unsupported child statement: " + child.getClass().getName()); + } + } + return new Batch( + batchStatement.getBatchType().getProtocolCode(), + queriesOrIds, + values, + consistencyCode, + serialConsistencyCode, + timestamp, + (keyspace == null) ? null : keyspace.asInternal()); + } else { + throw new IllegalArgumentException( + "Unsupported statement type: " + statement.getClass().getName()); + } + } + + public static List encode( + List values, CodecRegistry codecRegistry, ProtocolVersion protocolVersion) { + if (values.isEmpty()) { + return Collections.emptyList(); + } else { + ByteBuffer[] encodedValues = new ByteBuffer[values.size()]; + int i = 0; + for (Object value : values) { + encodedValues[i++] = (value == null) ? null : encode(value, codecRegistry, protocolVersion); + } + return NullAllowingImmutableList.of(encodedValues); + } + } + + public static Map encode( + Map values, + CodecRegistry codecRegistry, + ProtocolVersion protocolVersion) { + if (values.isEmpty()) { + return Collections.emptyMap(); + } else { + NullAllowingImmutableMap.Builder encodedValues = + NullAllowingImmutableMap.builder(values.size()); + for (Map.Entry entry : values.entrySet()) { + if (entry.getValue() == null) { + encodedValues.put(entry.getKey().asInternal(), null); + } else { + encodedValues.put( + entry.getKey().asInternal(), + encode(entry.getValue(), codecRegistry, protocolVersion)); + } + } + return encodedValues.build(); + } + } + + public static ByteBuffer encode( + Object value, CodecRegistry codecRegistry, ProtocolVersion protocolVersion) { + if (value instanceof Token) { + if (value instanceof Murmur3Token) { + return TypeCodecs.BIGINT.encode(((Murmur3Token) value).getValue(), protocolVersion); + } else if (value instanceof ByteOrderedToken) { + return TypeCodecs.BLOB.encode(((ByteOrderedToken) value).getValue(), protocolVersion); + } else if (value instanceof RandomToken) { + return TypeCodecs.VARINT.encode(((RandomToken) value).getValue(), protocolVersion); + } else { + throw new IllegalArgumentException("Unsupported token type " + value.getClass()); + } + } else { + return codecRegistry.codecFor(value).encode(value, protocolVersion); + } + } + + public static void ensureAllSet(BoundStatement boundStatement) { + for (int i = 0; i < boundStatement.size(); i++) { + if (!boundStatement.isSet(i)) { + throw new IllegalStateException( + "Unset value at index " + + i + + ". " + + "If you want this value to be null, please set it to null explicitly."); + } + } + } + + public static void ensureAllSet(BatchStatement batchStatement) { + for (BatchableStatement batchableStatement : batchStatement) { + if (batchableStatement instanceof BoundStatement) { + ensureAllSet(((BoundStatement) batchableStatement)); + } + } + } + + public static AsyncResultSet toResultSet( + Result result, + ExecutionInfo executionInfo, + CqlSession session, + InternalDriverContext context) { + if (result instanceof Rows) { + Rows rows = (Rows) result; + Statement statement = executionInfo.getStatement(); + ColumnDefinitions columnDefinitions = getResultDefinitions(rows, statement, context); + return new DefaultAsyncResultSet( + columnDefinitions, executionInfo, rows.getData(), session, context); + } else if (result instanceof Prepared) { + // This should never happen + throw new IllegalArgumentException("Unexpected PREPARED response to a CQL query"); + } else { + // Void, SetKeyspace, SchemaChange + return DefaultAsyncResultSet.empty(executionInfo); + } + } + + public static ColumnDefinitions getResultDefinitions( + Rows rows, Statement statement, InternalDriverContext context) { + RowsMetadata rowsMetadata = rows.getMetadata(); + if (rowsMetadata.columnSpecs.isEmpty()) { + // If the response has no metadata, it means the request had SKIP_METADATA set, the driver + // only ever does that for bound statements. + BoundStatement boundStatement = (BoundStatement) statement; + return boundStatement.getPreparedStatement().getResultSetDefinitions(); + } else { + // The response has metadata, always use it above anything else we might have locally. + ColumnDefinitions definitions = toColumnDefinitions(rowsMetadata, context); + // In addition, if the server signaled a schema change (see CASSANDRA-10786), update the + // prepared statement's copy of the metadata + if (rowsMetadata.newResultMetadataId != null) { + BoundStatement boundStatement = (BoundStatement) statement; + PreparedStatement preparedStatement = boundStatement.getPreparedStatement(); + preparedStatement.setResultMetadata( + ByteBuffer.wrap(rowsMetadata.newResultMetadataId).asReadOnlyBuffer(), definitions); + } + return definitions; + } + } + + public static DefaultPreparedStatement toPreparedStatement( + Prepared response, PrepareRequest request, InternalDriverContext context) { + return new DefaultPreparedStatement( + ByteBuffer.wrap(response.preparedQueryId).asReadOnlyBuffer(), + request.getQuery(), + toColumnDefinitions(response.variablesMetadata, context), + asList(response.variablesMetadata.pkIndices), + (response.resultMetadataId == null) + ? null + : ByteBuffer.wrap(response.resultMetadataId).asReadOnlyBuffer(), + toColumnDefinitions(response.resultMetadata, context), + request.getKeyspace(), + NullAllowingImmutableMap.copyOf(request.getCustomPayload()), + request.getExecutionProfileNameForBoundStatements(), + request.getExecutionProfileForBoundStatements(), + request.getRoutingKeyspaceForBoundStatements(), + request.getRoutingKeyForBoundStatements(), + request.getRoutingTokenForBoundStatements(), + NullAllowingImmutableMap.copyOf(request.getCustomPayloadForBoundStatements()), + request.areBoundStatementsIdempotent(), + request.getTimeoutForBoundStatements(), + request.getPagingStateForBoundStatements(), + request.getPageSizeForBoundStatements(), + request.getConsistencyLevelForBoundStatements(), + request.getSerialConsistencyLevelForBoundStatements(), + request.areBoundStatementsTracing(), + context.getCodecRegistry(), + context.getProtocolVersion()); + } + + public static ColumnDefinitions toColumnDefinitions( + RowsMetadata metadata, InternalDriverContext context) { + ColumnDefinition[] values = new ColumnDefinition[metadata.columnSpecs.size()]; + int i = 0; + for (ColumnSpec columnSpec : metadata.columnSpecs) { + values[i++] = new DefaultColumnDefinition(columnSpec, context); + } + return DefaultColumnDefinitions.valueOf(ImmutableList.copyOf(values)); + } + + public static List asList(int[] pkIndices) { + if (pkIndices == null || pkIndices.length == 0) { + return Collections.emptyList(); + } else { + return Ints.asList(pkIndices); + } + } + + public static CoordinatorException toThrowable( + Node node, Error errorMessage, InternalDriverContext context) { + switch (errorMessage.code) { + case ProtocolConstants.ErrorCode.UNPREPARED: + throw new AssertionError( + "UNPREPARED should be handled as a special case, not turned into an exception"); + case ProtocolConstants.ErrorCode.SERVER_ERROR: + return new ServerError(node, errorMessage.message); + case ProtocolConstants.ErrorCode.PROTOCOL_ERROR: + return new ProtocolError(node, errorMessage.message); + case ProtocolConstants.ErrorCode.AUTH_ERROR: + // This method is used for query execution, authentication errors should only happen during + // connection init + return new ProtocolError( + node, "Unexpected authentication error (" + errorMessage.message + ")"); + case ProtocolConstants.ErrorCode.UNAVAILABLE: + Unavailable unavailable = (Unavailable) errorMessage; + return new UnavailableException( + node, + context.getConsistencyLevelRegistry().codeToLevel(unavailable.consistencyLevel), + unavailable.required, + unavailable.alive); + case ProtocolConstants.ErrorCode.OVERLOADED: + return new OverloadedException(node); + case ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING: + return new BootstrappingException(node); + case ProtocolConstants.ErrorCode.TRUNCATE_ERROR: + return new TruncateException(node, errorMessage.message); + case ProtocolConstants.ErrorCode.WRITE_TIMEOUT: + WriteTimeout writeTimeout = (WriteTimeout) errorMessage; + return new WriteTimeoutException( + node, + context.getConsistencyLevelRegistry().codeToLevel(writeTimeout.consistencyLevel), + writeTimeout.received, + writeTimeout.blockFor, + context.getWriteTypeRegistry().fromName(writeTimeout.writeType)); + case ProtocolConstants.ErrorCode.READ_TIMEOUT: + ReadTimeout readTimeout = (ReadTimeout) errorMessage; + return new ReadTimeoutException( + node, + context.getConsistencyLevelRegistry().codeToLevel(readTimeout.consistencyLevel), + readTimeout.received, + readTimeout.blockFor, + readTimeout.dataPresent); + case ProtocolConstants.ErrorCode.READ_FAILURE: + ReadFailure readFailure = (ReadFailure) errorMessage; + return new ReadFailureException( + node, + context.getConsistencyLevelRegistry().codeToLevel(readFailure.consistencyLevel), + readFailure.received, + readFailure.blockFor, + readFailure.numFailures, + readFailure.dataPresent, + readFailure.reasonMap); + case ProtocolConstants.ErrorCode.FUNCTION_FAILURE: + return new FunctionFailureException(node, errorMessage.message); + case ProtocolConstants.ErrorCode.WRITE_FAILURE: + WriteFailure writeFailure = (WriteFailure) errorMessage; + return new WriteFailureException( + node, + context.getConsistencyLevelRegistry().codeToLevel(writeFailure.consistencyLevel), + writeFailure.received, + writeFailure.blockFor, + context.getWriteTypeRegistry().fromName(writeFailure.writeType), + writeFailure.numFailures, + writeFailure.reasonMap); + case ProtocolConstants.ErrorCode.SYNTAX_ERROR: + return new SyntaxError(node, errorMessage.message); + case ProtocolConstants.ErrorCode.UNAUTHORIZED: + return new UnauthorizedException(node, errorMessage.message); + case ProtocolConstants.ErrorCode.INVALID: + return new InvalidQueryException(node, errorMessage.message); + case ProtocolConstants.ErrorCode.CONFIG_ERROR: + return new InvalidConfigurationInQueryException(node, errorMessage.message); + case ProtocolConstants.ErrorCode.ALREADY_EXISTS: + AlreadyExists alreadyExists = (AlreadyExists) errorMessage; + return new AlreadyExistsException(node, alreadyExists.keyspace, alreadyExists.table); + default: + return new ProtocolError(node, "Unknown error code: " + errorMessage.code); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareAsyncProcessor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareAsyncProcessor.java new file mode 100644 index 00000000000..4e0b51fe482 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareAsyncProcessor.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.cql.PrepareRequest; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.internal.core.session.RequestProcessor; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.cache.Cache; +import com.datastax.oss.driver.shaded.guava.common.cache.CacheBuilder; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class CqlPrepareAsyncProcessor + implements RequestProcessor> { + + protected final Cache> cache; + + public CqlPrepareAsyncProcessor() { + this(CacheBuilder.newBuilder().weakValues().build()); + } + + protected CqlPrepareAsyncProcessor( + Cache> cache) { + this.cache = cache; + } + + @Override + public boolean canProcess(Request request, GenericType resultType) { + return request instanceof PrepareRequest && resultType.equals(PrepareRequest.ASYNC); + } + + @Override + public CompletionStage process( + PrepareRequest request, + DefaultSession session, + InternalDriverContext context, + String sessionLogPrefix) { + + try { + CompletableFuture result = cache.getIfPresent(request); + if (result == null) { + CompletableFuture mine = new CompletableFuture<>(); + result = cache.get(request, () -> mine); + if (result == mine) { + new CqlPrepareHandler(request, session, context, sessionLogPrefix) + .handle() + .whenComplete( + (preparedStatement, error) -> { + if (error != null) { + mine.completeExceptionally(error); + cache.invalidate(request); // Make sure failure isn't cached indefinitely + } else { + mine.complete(preparedStatement); + } + }); + } + } + return result; + } catch (ExecutionException e) { + return CompletableFutures.failedFuture(e.getCause()); + } + } + + @Override + public CompletionStage newFailure(RuntimeException error) { + return CompletableFutures.failedFuture(error); + } + + public Cache> getCache() { + return cache; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareHandler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareHandler.java new file mode 100644 index 00000000000..cc9c9ea0cfb --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareHandler.java @@ -0,0 +1,463 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DriverTimeoutException; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.RequestThrottlingException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.PrepareRequest; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; +import com.datastax.oss.driver.api.core.retry.RetryDecision; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.servererrors.BootstrappingException; +import com.datastax.oss.driver.api.core.servererrors.CoordinatorException; +import com.datastax.oss.driver.api.core.servererrors.FunctionFailureException; +import com.datastax.oss.driver.api.core.servererrors.ProtocolError; +import com.datastax.oss.driver.api.core.servererrors.QueryValidationException; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.api.core.session.throttling.Throttled; +import com.datastax.oss.driver.internal.core.DefaultProtocolFeature; +import com.datastax.oss.driver.internal.core.ProtocolVersionRegistry; +import com.datastax.oss.driver.internal.core.adminrequest.ThrottledAdminRequestHandler; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.ResponseCallback; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.request.Prepare; +import com.datastax.oss.protocol.internal.response.Error; +import com.datastax.oss.protocol.internal.response.result.Prepared; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.util.Timeout; +import io.netty.util.Timer; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import java.time.Duration; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Handles the lifecycle of the preparation of a CQL statement. */ +@ThreadSafe +public class CqlPrepareHandler implements Throttled { + + private static final Logger LOG = LoggerFactory.getLogger(CqlPrepareHandler.class); + + private final long startTimeNanos; + private final String logPrefix; + private final PrepareRequest request; + private final DefaultSession session; + private final InternalDriverContext context; + private final DriverExecutionProfile executionProfile; + private final Queue queryPlan; + protected final CompletableFuture result; + private final Message message; + private final Timer timer; + private final Duration timeout; + private final Timeout scheduledTimeout; + private final RetryPolicy retryPolicy; + private final RequestThrottler throttler; + private final Boolean prepareOnAllNodes; + private volatile InitialPrepareCallback initialCallback; + + // The errors on the nodes that were already tried (lazily initialized on the first error). + // We don't use a map because nodes can appear multiple times. + private volatile List> errors; + + protected CqlPrepareHandler( + PrepareRequest request, + DefaultSession session, + InternalDriverContext context, + String sessionLogPrefix) { + + this.startTimeNanos = System.nanoTime(); + this.logPrefix = sessionLogPrefix + "|" + this.hashCode(); + LOG.trace("[{}] Creating new handler for prepare request {}", logPrefix, request); + + this.request = request; + this.session = session; + this.context = context; + this.executionProfile = Conversions.resolveExecutionProfile(request, context); + this.queryPlan = + context + .getLoadBalancingPolicyWrapper() + .newQueryPlan(request, executionProfile.getName(), session); + this.retryPolicy = context.getRetryPolicy(executionProfile.getName()); + + this.result = new CompletableFuture<>(); + this.result.exceptionally( + t -> { + try { + if (t instanceof CancellationException) { + cancelTimeout(); + } + } catch (Throwable t2) { + Loggers.warnWithException(LOG, "[{}] Uncaught exception", logPrefix, t2); + } + return null; + }); + ProtocolVersion protocolVersion = context.getProtocolVersion(); + ProtocolVersionRegistry registry = context.getProtocolVersionRegistry(); + CqlIdentifier keyspace = request.getKeyspace(); + if (keyspace != null + && !registry.supports(protocolVersion, DefaultProtocolFeature.PER_REQUEST_KEYSPACE)) { + throw new IllegalArgumentException( + "Can't use per-request keyspace with protocol " + protocolVersion); + } + this.message = + new Prepare(request.getQuery(), (keyspace == null) ? null : keyspace.asInternal()); + this.timer = context.getNettyOptions().getTimer(); + + this.timeout = + request.getTimeout() != null + ? request.getTimeout() + : executionProfile.getDuration(DefaultDriverOption.REQUEST_TIMEOUT); + this.scheduledTimeout = scheduleTimeout(timeout); + this.prepareOnAllNodes = executionProfile.getBoolean(DefaultDriverOption.PREPARE_ON_ALL_NODES); + + this.throttler = context.getRequestThrottler(); + this.throttler.register(this); + } + + @Override + public void onThrottleReady(boolean wasDelayed) { + if (wasDelayed) { + session + .getMetricUpdater() + .updateTimer( + DefaultSessionMetric.THROTTLING_DELAY, + executionProfile.getName(), + System.nanoTime() - startTimeNanos, + TimeUnit.NANOSECONDS); + } + sendRequest(null, 0); + } + + public CompletableFuture handle() { + return result; + } + + private Timeout scheduleTimeout(Duration timeoutDuration) { + if (timeoutDuration.toNanos() > 0) { + return this.timer.newTimeout( + (Timeout timeout1) -> { + setFinalError(new DriverTimeoutException("Query timed out after " + timeoutDuration)); + if (initialCallback != null) { + initialCallback.cancel(); + } + }, + timeoutDuration.toNanos(), + TimeUnit.NANOSECONDS); + } else { + return null; + } + } + + private void cancelTimeout() { + if (this.scheduledTimeout != null) { + this.scheduledTimeout.cancel(); + } + } + + private void sendRequest(Node node, int retryCount) { + if (result.isDone()) { + return; + } + DriverChannel channel = null; + if (node == null || (channel = session.getChannel(node, logPrefix)) == null) { + while (!result.isDone() && (node = queryPlan.poll()) != null) { + channel = session.getChannel(node, logPrefix); + if (channel != null) { + break; + } + } + } + if (channel == null) { + setFinalError(AllNodesFailedException.fromErrors(this.errors)); + } else { + InitialPrepareCallback initialPrepareCallback = + new InitialPrepareCallback(node, channel, retryCount); + channel + .write(message, false, request.getCustomPayload(), initialPrepareCallback) + .addListener(initialPrepareCallback); + } + } + + private void recordError(Node node, Throwable error) { + // Use a local variable to do only a single single volatile read in the nominal case + List> errorsSnapshot = this.errors; + if (errorsSnapshot == null) { + synchronized (CqlPrepareHandler.this) { + errorsSnapshot = this.errors; + if (errorsSnapshot == null) { + this.errors = errorsSnapshot = new CopyOnWriteArrayList<>(); + } + } + } + errorsSnapshot.add(new AbstractMap.SimpleEntry<>(node, error)); + } + + private void setFinalResult(Prepared prepared) { + + // Whatever happens below, we're done with this stream id + throttler.signalSuccess(this); + + DefaultPreparedStatement preparedStatement = + Conversions.toPreparedStatement(prepared, request, context); + + session + .getRepreparePayloads() + .put(preparedStatement.getId(), preparedStatement.getRepreparePayload()); + if (prepareOnAllNodes) { + prepareOnOtherNodes() + .thenRun( + () -> { + LOG.trace( + "[{}] Done repreparing on other nodes, completing the request", logPrefix); + result.complete(preparedStatement); + }) + .exceptionally( + error -> { + result.completeExceptionally(error); + return null; + }); + } else { + LOG.trace("[{}] Prepare on all nodes is disabled, completing the request", logPrefix); + result.complete(preparedStatement); + } + } + + private CompletionStage prepareOnOtherNodes() { + List> otherNodesFutures = new ArrayList<>(); + // Only process the rest of the query plan. Any node before that is either the coordinator, or + // a node that failed (we assume that retrying right now has little chance of success). + for (Node node : queryPlan) { + otherNodesFutures.add(prepareOnOtherNode(node)); + } + return CompletableFutures.allDone(otherNodesFutures); + } + + // Try to reprepare on another node, after the initial query has succeeded. Errors are not + // blocking, the preparation will be retried later on that node. Simply warn and move on. + private CompletionStage prepareOnOtherNode(Node node) { + LOG.trace("[{}] Repreparing on {}", logPrefix, node); + DriverChannel channel = session.getChannel(node, logPrefix); + if (channel == null) { + LOG.trace("[{}] Could not get a channel to reprepare on {}, skipping", logPrefix, node); + return CompletableFuture.completedFuture(null); + } else { + ThrottledAdminRequestHandler handler = + new ThrottledAdminRequestHandler( + channel, + message, + request.getCustomPayload(), + timeout, + throttler, + session.getMetricUpdater(), + logPrefix, + message.toString()); + return handler + .start() + .handle( + (result, error) -> { + if (error == null) { + LOG.trace("[{}] Successfully reprepared on {}", logPrefix, node); + } else { + Loggers.warnWithException( + LOG, "[{}] Error while repreparing on {}", node, logPrefix, error); + } + return null; + }); + } + } + + @Override + public void onThrottleFailure(@NonNull RequestThrottlingException error) { + session + .getMetricUpdater() + .incrementCounter(DefaultSessionMetric.THROTTLING_ERRORS, executionProfile.getName()); + setFinalError(error); + } + + private void setFinalError(Throwable error) { + if (result.completeExceptionally(error)) { + cancelTimeout(); + if (error instanceof DriverTimeoutException) { + throttler.signalTimeout(this); + } else if (!(error instanceof RequestThrottlingException)) { + throttler.signalError(this, error); + } + } + } + + private class InitialPrepareCallback + implements ResponseCallback, GenericFutureListener> { + private final Node node; + private final DriverChannel channel; + // How many times we've invoked the retry policy and it has returned a "retry" decision (0 for + // the first attempt of each execution). + private final int retryCount; + + private InitialPrepareCallback(Node node, DriverChannel channel, int retryCount) { + this.node = node; + this.channel = channel; + this.retryCount = retryCount; + } + + // this gets invoked once the write completes. + @Override + public void operationComplete(Future future) { + if (!future.isSuccess()) { + LOG.trace( + "[{}] Failed to send request on {}, trying next node (cause: {})", + logPrefix, + node, + future.cause().toString()); + recordError(node, future.cause()); + sendRequest(null, retryCount); // try next host + } else { + if (result.isDone()) { + // Might happen if the timeout just fired + cancel(); + } else { + LOG.trace("[{}] Request sent to {}", logPrefix, node); + initialCallback = this; + } + } + } + + @Override + public void onResponse(Frame responseFrame) { + if (result.isDone()) { + return; + } + try { + Message responseMessage = responseFrame.message; + if (responseMessage instanceof Prepared) { + LOG.trace("[{}] Got result, completing", logPrefix); + setFinalResult((Prepared) responseMessage); + } else if (responseMessage instanceof Error) { + LOG.trace("[{}] Got error response, processing", logPrefix); + processErrorResponse((Error) responseMessage); + } else { + setFinalError(new IllegalStateException("Unexpected response " + responseMessage)); + } + } catch (Throwable t) { + setFinalError(t); + } + } + + private void processErrorResponse(Error errorMessage) { + if (errorMessage.code == ProtocolConstants.ErrorCode.UNPREPARED + || errorMessage.code == ProtocolConstants.ErrorCode.ALREADY_EXISTS + || errorMessage.code == ProtocolConstants.ErrorCode.READ_FAILURE + || errorMessage.code == ProtocolConstants.ErrorCode.READ_TIMEOUT + || errorMessage.code == ProtocolConstants.ErrorCode.WRITE_FAILURE + || errorMessage.code == ProtocolConstants.ErrorCode.WRITE_TIMEOUT + || errorMessage.code == ProtocolConstants.ErrorCode.UNAVAILABLE + || errorMessage.code == ProtocolConstants.ErrorCode.TRUNCATE_ERROR) { + setFinalError( + new IllegalStateException( + "Unexpected server error for a PREPARE query" + errorMessage)); + return; + } + CoordinatorException error = Conversions.toThrowable(node, errorMessage, context); + if (error instanceof BootstrappingException) { + LOG.trace("[{}] {} is bootstrapping, trying next node", logPrefix, node); + recordError(node, error); + sendRequest(null, retryCount); + } else if (error instanceof QueryValidationException + || error instanceof FunctionFailureException + || error instanceof ProtocolError) { + LOG.trace("[{}] Unrecoverable error, rethrowing", logPrefix); + setFinalError(error); + } else { + // Because prepare requests are known to always be idempotent, we call the retry policy + // directly, without checking the flag. + RetryDecision decision = retryPolicy.onErrorResponse(request, error, retryCount); + processRetryDecision(decision, error); + } + } + + private void processRetryDecision(RetryDecision decision, Throwable error) { + LOG.trace("[{}] Processing retry decision {}", logPrefix, decision); + switch (decision) { + case RETRY_SAME: + recordError(node, error); + sendRequest(node, retryCount + 1); + break; + case RETRY_NEXT: + recordError(node, error); + sendRequest(null, retryCount + 1); + break; + case RETHROW: + setFinalError(error); + break; + case IGNORE: + setFinalError( + new IllegalArgumentException( + "IGNORE decisions are not allowed for prepare requests, " + + "please fix your retry policy.")); + break; + } + } + + @Override + public void onFailure(Throwable error) { + if (result.isDone()) { + return; + } + LOG.trace("[{}] Request failure, processing: {}", logPrefix, error.toString()); + RetryDecision decision = retryPolicy.onRequestAborted(request, error, retryCount); + processRetryDecision(decision, error); + } + + public void cancel() { + try { + if (!channel.closeFuture().isDone()) { + this.channel.cancel(this); + } + } catch (Throwable t) { + Loggers.warnWithException(LOG, "[{}] Error cancelling", logPrefix, t); + } + } + + @Override + public String toString() { + return logPrefix; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareSyncProcessor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareSyncProcessor.java new file mode 100644 index 00000000000..90e20f72394 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareSyncProcessor.java @@ -0,0 +1,71 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.cql.PrepareRequest; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.internal.core.session.RequestProcessor; +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.cache.Cache; +import java.util.concurrent.CompletableFuture; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class CqlPrepareSyncProcessor + implements RequestProcessor { + + private final CqlPrepareAsyncProcessor asyncProcessor; + + /** + * Note: if you also register a {@link CqlPrepareAsyncProcessor} with your session, make sure that + * you pass that same instance to this constructor. This is necessary for proper behavior of the + * prepared statement cache. + */ + public CqlPrepareSyncProcessor(CqlPrepareAsyncProcessor asyncProcessor) { + this.asyncProcessor = asyncProcessor; + } + + @Override + public boolean canProcess(Request request, GenericType resultType) { + return request instanceof PrepareRequest && resultType.equals(PrepareRequest.SYNC); + } + + @Override + public PreparedStatement process( + PrepareRequest request, + DefaultSession session, + InternalDriverContext context, + String sessionLogPrefix) { + + BlockingOperation.checkNotDriverThread(); + return CompletableFutures.getUninterruptibly( + asyncProcessor.process(request, session, context, sessionLogPrefix)); + } + + public Cache> getCache() { + return asyncProcessor.getCache(); + } + + @Override + public PreparedStatement newFailure(RuntimeException error) { + throw error; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlRequestAsyncProcessor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlRequestAsyncProcessor.java new file mode 100644 index 00000000000..837c0062602 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlRequestAsyncProcessor.java @@ -0,0 +1,51 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.internal.core.session.RequestProcessor; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import java.util.concurrent.CompletionStage; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class CqlRequestAsyncProcessor + implements RequestProcessor, CompletionStage> { + + @Override + public boolean canProcess(Request request, GenericType resultType) { + return request instanceof Statement && resultType.equals(Statement.ASYNC); + } + + @Override + public CompletionStage process( + Statement request, + DefaultSession session, + InternalDriverContext context, + String sessionLogPrefix) { + return new CqlRequestHandler(request, session, context, sessionLogPrefix).handle(); + } + + @Override + public CompletionStage newFailure(RuntimeException error) { + return CompletableFutures.failedFuture(error); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandler.java new file mode 100644 index 00000000000..63d8770d5b3 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandler.java @@ -0,0 +1,883 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.DriverTimeoutException; +import com.datastax.oss.driver.api.core.RequestThrottlingException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.FrameTooLongException; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; +import com.datastax.oss.driver.api.core.retry.RetryDecision; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.servererrors.BootstrappingException; +import com.datastax.oss.driver.api.core.servererrors.CoordinatorException; +import com.datastax.oss.driver.api.core.servererrors.FunctionFailureException; +import com.datastax.oss.driver.api.core.servererrors.ProtocolError; +import com.datastax.oss.driver.api.core.servererrors.QueryValidationException; +import com.datastax.oss.driver.api.core.servererrors.ReadTimeoutException; +import com.datastax.oss.driver.api.core.servererrors.UnavailableException; +import com.datastax.oss.driver.api.core.servererrors.WriteTimeoutException; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.api.core.session.throttling.Throttled; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import com.datastax.oss.driver.internal.core.adminrequest.ThrottledAdminRequestHandler; +import com.datastax.oss.driver.internal.core.adminrequest.UnexpectedResponseException; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.ResponseCallback; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.metrics.NodeMetricUpdater; +import com.datastax.oss.driver.internal.core.metrics.SessionMetricUpdater; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.internal.core.session.RepreparePayload; +import com.datastax.oss.driver.internal.core.tracker.NoopRequestTracker; +import com.datastax.oss.driver.internal.core.tracker.RequestLogger; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.collection.QueryPlan; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.request.Prepare; +import com.datastax.oss.protocol.internal.response.Error; +import com.datastax.oss.protocol.internal.response.Result; +import com.datastax.oss.protocol.internal.response.error.Unprepared; +import com.datastax.oss.protocol.internal.response.result.Rows; +import com.datastax.oss.protocol.internal.response.result.SchemaChange; +import com.datastax.oss.protocol.internal.response.result.SetKeyspace; +import com.datastax.oss.protocol.internal.response.result.Void; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.handler.codec.EncoderException; +import io.netty.util.Timeout; +import io.netty.util.Timer; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class CqlRequestHandler implements Throttled { + + private static final Logger LOG = LoggerFactory.getLogger(CqlRequestHandler.class); + private static final long NANOTIME_NOT_MEASURED_YET = -1; + + private final long startTimeNanos; + private final String logPrefix; + private final Statement statement; + private final DefaultSession session; + private final CqlIdentifier keyspace; + private final InternalDriverContext context; + @NonNull private final DriverExecutionProfile executionProfile; + private final boolean isIdempotent; + protected final CompletableFuture result; + private final Message message; + private final Timer timer; + /** + * How many speculative executions are currently running (including the initial execution). We + * track this in order to know when to fail the request if all executions have reached the end of + * the query plan. + */ + private final AtomicInteger activeExecutionsCount; + /** + * How many speculative executions have started (excluding the initial execution), whether they + * have completed or not. We track this in order to fill {@link + * ExecutionInfo#getSpeculativeExecutionCount()}. + */ + private final AtomicInteger startedSpeculativeExecutionsCount; + + private final Duration timeout; + final Timeout scheduledTimeout; + final List scheduledExecutions; + private final List inFlightCallbacks; + private final RetryPolicy retryPolicy; + private final SpeculativeExecutionPolicy speculativeExecutionPolicy; + private final RequestThrottler throttler; + private final RequestTracker requestTracker; + private final SessionMetricUpdater sessionMetricUpdater; + + // The errors on the nodes that were already tried (lazily initialized on the first error). + // We don't use a map because nodes can appear multiple times. + private volatile List> errors; + + protected CqlRequestHandler( + Statement statement, + DefaultSession session, + InternalDriverContext context, + String sessionLogPrefix) { + + this.startTimeNanos = System.nanoTime(); + this.logPrefix = sessionLogPrefix + "|" + this.hashCode(); + LOG.trace("[{}] Creating new handler for request {}", logPrefix, statement); + + this.statement = statement; + this.session = session; + this.keyspace = session.getKeyspace().orElse(null); + this.context = context; + this.executionProfile = Conversions.resolveExecutionProfile(statement, context); + this.retryPolicy = context.getRetryPolicy(executionProfile.getName()); + this.speculativeExecutionPolicy = + context.getSpeculativeExecutionPolicy(executionProfile.getName()); + Boolean statementIsIdempotent = statement.isIdempotent(); + this.isIdempotent = + (statementIsIdempotent == null) + ? executionProfile.getBoolean(DefaultDriverOption.REQUEST_DEFAULT_IDEMPOTENCE) + : statementIsIdempotent; + this.result = new CompletableFuture<>(); + this.result.exceptionally( + t -> { + try { + if (t instanceof CancellationException) { + cancelScheduledTasks(); + } + } catch (Throwable t2) { + Loggers.warnWithException(LOG, "[{}] Uncaught exception", logPrefix, t2); + } + return null; + }); + this.message = Conversions.toMessage(statement, executionProfile, context); + this.timer = context.getNettyOptions().getTimer(); + + this.timeout = + statement.getTimeout() != null + ? statement.getTimeout() + : executionProfile.getDuration(DefaultDriverOption.REQUEST_TIMEOUT); + this.scheduledTimeout = scheduleTimeout(timeout); + + this.activeExecutionsCount = new AtomicInteger(1); + this.startedSpeculativeExecutionsCount = new AtomicInteger(0); + this.scheduledExecutions = isIdempotent ? new CopyOnWriteArrayList<>() : null; + this.inFlightCallbacks = new CopyOnWriteArrayList<>(); + + this.requestTracker = context.getRequestTracker(); + this.sessionMetricUpdater = session.getMetricUpdater(); + + this.throttler = context.getRequestThrottler(); + this.throttler.register(this); + } + + @Override + public void onThrottleReady(boolean wasDelayed) { + if (wasDelayed + // avoid call to nanoTime() if metric is disabled: + && sessionMetricUpdater.isEnabled( + DefaultSessionMetric.THROTTLING_DELAY, executionProfile.getName())) { + sessionMetricUpdater.updateTimer( + DefaultSessionMetric.THROTTLING_DELAY, + executionProfile.getName(), + System.nanoTime() - startTimeNanos, + TimeUnit.NANOSECONDS); + } + Queue queryPlan = + this.statement.getNode() != null + ? new QueryPlan(this.statement.getNode()) + : context + .getLoadBalancingPolicyWrapper() + .newQueryPlan(statement, executionProfile.getName(), session); + sendRequest(null, queryPlan, 0, 0, true); + } + + public CompletionStage handle() { + return result; + } + + private Timeout scheduleTimeout(Duration timeoutDuration) { + if (timeoutDuration.toNanos() > 0) { + try { + return this.timer.newTimeout( + (Timeout timeout1) -> + setFinalError( + new DriverTimeoutException("Query timed out after " + timeoutDuration), + null, + -1), + timeoutDuration.toNanos(), + TimeUnit.NANOSECONDS); + } catch (IllegalStateException e) { + // If we raced with session shutdown the timer might be closed already, rethrow with a more + // explicit message + result.completeExceptionally( + ("cannot be started once stopped".equals(e.getMessage())) + ? new IllegalStateException("Session is closed") + : e); + } + } + return null; + } + + /** + * Sends the request to the next available node. + * + * @param retriedNode if not null, it will be attempted first before the rest of the query plan. + * @param queryPlan the list of nodes to try (shared with all other executions) + * @param currentExecutionIndex 0 for the initial execution, 1 for the first speculative one, etc. + * @param retryCount the number of times that the retry policy was invoked for this execution + * already (note that some internal retries don't go through the policy, and therefore don't + * increment this counter) + * @param scheduleNextExecution whether to schedule the next speculative execution + */ + private void sendRequest( + Node retriedNode, + Queue queryPlan, + int currentExecutionIndex, + int retryCount, + boolean scheduleNextExecution) { + if (result.isDone()) { + return; + } + Node node = retriedNode; + DriverChannel channel = null; + if (node == null || (channel = session.getChannel(node, logPrefix)) == null) { + while (!result.isDone() && (node = queryPlan.poll()) != null) { + channel = session.getChannel(node, logPrefix); + if (channel != null) { + break; + } + } + } + if (channel == null) { + // We've reached the end of the query plan without finding any node to write to + if (!result.isDone() && activeExecutionsCount.decrementAndGet() == 0) { + // We're the last execution so fail the result + setFinalError(AllNodesFailedException.fromErrors(this.errors), null, -1); + } + } else { + NodeResponseCallback nodeResponseCallback = + new NodeResponseCallback( + node, + queryPlan, + channel, + currentExecutionIndex, + retryCount, + scheduleNextExecution, + logPrefix); + channel + .write(message, statement.isTracing(), statement.getCustomPayload(), nodeResponseCallback) + .addListener(nodeResponseCallback); + } + } + + private void recordError(Node node, Throwable error) { + // Use a local variable to do only a single single volatile read in the nominal case + List> errorsSnapshot = this.errors; + if (errorsSnapshot == null) { + synchronized (CqlRequestHandler.this) { + errorsSnapshot = this.errors; + if (errorsSnapshot == null) { + this.errors = errorsSnapshot = new CopyOnWriteArrayList<>(); + } + } + } + errorsSnapshot.add(new AbstractMap.SimpleEntry<>(node, error)); + } + + private void cancelScheduledTasks() { + if (this.scheduledTimeout != null) { + this.scheduledTimeout.cancel(); + } + if (scheduledExecutions != null) { + for (Timeout scheduledExecution : scheduledExecutions) { + scheduledExecution.cancel(); + } + } + for (NodeResponseCallback callback : inFlightCallbacks) { + callback.cancel(); + } + } + + private void setFinalResult( + Result resultMessage, + Frame responseFrame, + boolean schemaInAgreement, + NodeResponseCallback callback) { + try { + ExecutionInfo executionInfo = + buildExecutionInfo(callback, resultMessage, responseFrame, schemaInAgreement); + AsyncResultSet resultSet = + Conversions.toResultSet(resultMessage, executionInfo, session, context); + if (result.complete(resultSet)) { + cancelScheduledTasks(); + throttler.signalSuccess(this); + + // Only call nanoTime() if we're actually going to use it + long completionTimeNanos = NANOTIME_NOT_MEASURED_YET, + totalLatencyNanos = NANOTIME_NOT_MEASURED_YET; + if (!(requestTracker instanceof NoopRequestTracker)) { + completionTimeNanos = System.nanoTime(); + totalLatencyNanos = completionTimeNanos - startTimeNanos; + long nodeLatencyNanos = completionTimeNanos - callback.nodeStartTimeNanos; + requestTracker.onNodeSuccess( + statement, nodeLatencyNanos, executionProfile, callback.node); + requestTracker.onSuccess(statement, totalLatencyNanos, executionProfile, callback.node); + } + if (sessionMetricUpdater.isEnabled( + DefaultSessionMetric.CQL_REQUESTS, executionProfile.getName())) { + if (completionTimeNanos == NANOTIME_NOT_MEASURED_YET) { + completionTimeNanos = System.nanoTime(); + totalLatencyNanos = completionTimeNanos - startTimeNanos; + } + sessionMetricUpdater.updateTimer( + DefaultSessionMetric.CQL_REQUESTS, + executionProfile.getName(), + totalLatencyNanos, + TimeUnit.NANOSECONDS); + } + } + // log the warnings if they have NOT been disabled + if (!executionInfo.getWarnings().isEmpty() + && executionProfile.getBoolean(DefaultDriverOption.REQUEST_LOG_WARNINGS) + && LOG.isWarnEnabled()) { + logServerWarnings(executionInfo.getWarnings()); + } + } catch (Throwable error) { + setFinalError(error, callback.node, -1); + } + } + + private void logServerWarnings(List warnings) { + // use the RequestLogFormatter to format the query + StringBuilder statementString = new StringBuilder(); + context + .getRequestLogFormatter() + .appendRequest( + statement, + executionProfile.getInt( + DefaultDriverOption.REQUEST_LOGGER_MAX_QUERY_LENGTH, + RequestLogger.DEFAULT_REQUEST_LOGGER_MAX_QUERY_LENGTH), + executionProfile.getBoolean( + DefaultDriverOption.REQUEST_LOGGER_VALUES, + RequestLogger.DEFAULT_REQUEST_LOGGER_SHOW_VALUES), + executionProfile.getInt( + DefaultDriverOption.REQUEST_LOGGER_MAX_VALUES, + RequestLogger.DEFAULT_REQUEST_LOGGER_MAX_VALUES), + executionProfile.getInt( + DefaultDriverOption.REQUEST_LOGGER_MAX_VALUE_LENGTH, + RequestLogger.DEFAULT_REQUEST_LOGGER_MAX_VALUE_LENGTH), + statementString); + // log each warning separately + warnings.forEach( + (warning) -> + LOG.warn("Query '{}' generated server side warning(s): {}", statementString, warning)); + } + + private ExecutionInfo buildExecutionInfo( + NodeResponseCallback callback, + Result resultMessage, + Frame responseFrame, + boolean schemaInAgreement) { + ByteBuffer pagingState = + (resultMessage instanceof Rows) ? ((Rows) resultMessage).getMetadata().pagingState : null; + return new DefaultExecutionInfo( + statement, + callback.node, + startedSpeculativeExecutionsCount.get(), + callback.execution, + errors, + pagingState, + responseFrame, + schemaInAgreement, + session, + context, + executionProfile); + } + + @Override + public void onThrottleFailure(@NonNull RequestThrottlingException error) { + sessionMetricUpdater.incrementCounter( + DefaultSessionMetric.THROTTLING_ERRORS, executionProfile.getName()); + setFinalError(error, null, -1); + } + + private void setFinalError(Throwable error, Node node, int execution) { + if (error instanceof DriverException) { + ((DriverException) error) + .setExecutionInfo( + new DefaultExecutionInfo( + statement, + node, + startedSpeculativeExecutionsCount.get(), + execution, + errors, + null, + null, + true, + session, + context, + executionProfile)); + } + if (result.completeExceptionally(error)) { + cancelScheduledTasks(); + if (!(requestTracker instanceof NoopRequestTracker)) { + long latencyNanos = System.nanoTime() - startTimeNanos; + requestTracker.onError(statement, error, latencyNanos, executionProfile, node); + } + if (error instanceof DriverTimeoutException) { + throttler.signalTimeout(this); + sessionMetricUpdater.incrementCounter( + DefaultSessionMetric.CQL_CLIENT_TIMEOUTS, executionProfile.getName()); + } else if (!(error instanceof RequestThrottlingException)) { + throttler.signalError(this, error); + } + } + } + + /** + * Handles the interaction with a single node in the query plan. + * + *

An instance of this class is created each time we (re)try a node. + */ + private class NodeResponseCallback + implements ResponseCallback, GenericFutureListener> { + + private final long nodeStartTimeNanos = System.nanoTime(); + private final Node node; + private final Queue queryPlan; + private final DriverChannel channel; + // The identifier of the current execution (0 for the initial execution, 1 for the first + // speculative execution, etc.) + private final int execution; + // How many times we've invoked the retry policy and it has returned a "retry" decision (0 for + // the first attempt of each execution). + private final int retryCount; + private final boolean scheduleNextExecution; + private final String logPrefix; + + private NodeResponseCallback( + Node node, + Queue queryPlan, + DriverChannel channel, + int execution, + int retryCount, + boolean scheduleNextExecution, + String logPrefix) { + this.node = node; + this.queryPlan = queryPlan; + this.channel = channel; + this.execution = execution; + this.retryCount = retryCount; + this.scheduleNextExecution = scheduleNextExecution; + this.logPrefix = logPrefix + "|" + execution; + } + + // this gets invoked once the write completes. + @Override + public void operationComplete(Future future) throws Exception { + if (!future.isSuccess()) { + Throwable error = future.cause(); + if (error instanceof EncoderException + && error.getCause() instanceof FrameTooLongException) { + trackNodeError(node, error.getCause(), NANOTIME_NOT_MEASURED_YET); + setFinalError(error.getCause(), node, execution); + } else { + LOG.trace( + "[{}] Failed to send request on {}, trying next node (cause: {})", + logPrefix, + channel, + error); + recordError(node, error); + trackNodeError(node, error, NANOTIME_NOT_MEASURED_YET); + ((DefaultNode) node) + .getMetricUpdater() + .incrementCounter(DefaultNodeMetric.UNSENT_REQUESTS, executionProfile.getName()); + sendRequest( + null, queryPlan, execution, retryCount, scheduleNextExecution); // try next node + } + } else { + LOG.trace("[{}] Request sent on {}", logPrefix, channel); + if (result.isDone()) { + // If the handler completed since the last time we checked, cancel directly because we + // don't know if cancelScheduledTasks() has run yet + cancel(); + } else { + inFlightCallbacks.add(this); + if (scheduleNextExecution && isIdempotent) { + int nextExecution = execution + 1; + long nextDelay = + speculativeExecutionPolicy.nextExecution(node, keyspace, statement, nextExecution); + if (nextDelay >= 0) { + scheduleSpeculativeExecution(nextExecution, nextDelay); + } else { + LOG.trace( + "[{}] Speculative execution policy returned {}, no next execution", + logPrefix, + nextDelay); + } + } + } + } + } + + private void scheduleSpeculativeExecution(int index, long delay) { + LOG.trace("[{}] Scheduling speculative execution {} in {} ms", logPrefix, index, delay); + try { + scheduledExecutions.add( + timer.newTimeout( + (Timeout timeout1) -> { + if (!result.isDone()) { + LOG.trace( + "[{}] Starting speculative execution {}", + CqlRequestHandler.this.logPrefix, + index); + activeExecutionsCount.incrementAndGet(); + startedSpeculativeExecutionsCount.incrementAndGet(); + // Note that `node` is the first node of the execution, it might not be the + // "slow" one if there were retries, but in practice retries are rare. + ((DefaultNode) node) + .getMetricUpdater() + .incrementCounter( + DefaultNodeMetric.SPECULATIVE_EXECUTIONS, executionProfile.getName()); + sendRequest(null, queryPlan, index, 0, true); + } + }, + delay, + TimeUnit.MILLISECONDS)); + } catch (IllegalStateException e) { + // If we're racing with session shutdown, the timer might be stopped already. We don't want + // to schedule more executions anyway, so swallow the error. + if (!"cannot be started once stopped".equals(e.getMessage())) { + Loggers.warnWithException( + LOG, "[{}] Error while scheduling speculative execution", logPrefix, e); + } + } + } + + @Override + public void onResponse(Frame responseFrame) { + long nodeResponseTimeNanos = NANOTIME_NOT_MEASURED_YET; + NodeMetricUpdater nodeMetricUpdater = ((DefaultNode) node).getMetricUpdater(); + if (nodeMetricUpdater.isEnabled(DefaultNodeMetric.CQL_MESSAGES, executionProfile.getName())) { + nodeResponseTimeNanos = System.nanoTime(); + long nodeLatency = System.nanoTime() - nodeStartTimeNanos; + nodeMetricUpdater.updateTimer( + DefaultNodeMetric.CQL_MESSAGES, + executionProfile.getName(), + nodeLatency, + TimeUnit.NANOSECONDS); + } + inFlightCallbacks.remove(this); + if (result.isDone()) { + return; + } + try { + Message responseMessage = responseFrame.message; + if (responseMessage instanceof SchemaChange) { + SchemaChange schemaChange = (SchemaChange) responseMessage; + context + .getTopologyMonitor() + .checkSchemaAgreement() + .thenCombine( + context + .getMetadataManager() + .refreshSchema(schemaChange.keyspace, false, false) + .exceptionally( + error -> { + Loggers.warnWithException( + LOG, + "[{}] Error while refreshing schema after DDL query, " + + "new metadata might be incomplete", + logPrefix, + error); + return null; + }), + (schemaInAgreement, metadata) -> schemaInAgreement) + .whenComplete( + ((schemaInAgreement, error) -> + setFinalResult(schemaChange, responseFrame, schemaInAgreement, this))); + } else if (responseMessage instanceof SetKeyspace) { + SetKeyspace setKeyspace = (SetKeyspace) responseMessage; + session + .setKeyspace(CqlIdentifier.fromInternal(setKeyspace.keyspace)) + .whenComplete((v, error) -> setFinalResult(setKeyspace, responseFrame, true, this)); + } else if (responseMessage instanceof Result) { + LOG.trace("[{}] Got result, completing", logPrefix); + setFinalResult((Result) responseMessage, responseFrame, true, this); + } else if (responseMessage instanceof Error) { + LOG.trace("[{}] Got error response, processing", logPrefix); + processErrorResponse((Error) responseMessage); + } else { + trackNodeError( + node, + new IllegalStateException("Unexpected response " + responseMessage), + nodeResponseTimeNanos); + setFinalError( + new IllegalStateException("Unexpected response " + responseMessage), node, execution); + } + } catch (Throwable t) { + trackNodeError(node, t, nodeResponseTimeNanos); + setFinalError(t, node, execution); + } + } + + private void processErrorResponse(Error errorMessage) { + if (errorMessage.code == ProtocolConstants.ErrorCode.UNPREPARED) { + LOG.trace("[{}] Statement is not prepared on {}, repreparing", logPrefix, node); + ByteBuffer id = ByteBuffer.wrap(((Unprepared) errorMessage).id); + RepreparePayload repreparePayload = session.getRepreparePayloads().get(id); + if (repreparePayload == null) { + throw new IllegalStateException( + String.format( + "Tried to execute unprepared query %s but we don't have the data to reprepare it", + Bytes.toHexString(id))); + } + Prepare reprepareMessage = new Prepare(repreparePayload.query); + ThrottledAdminRequestHandler reprepareHandler = + new ThrottledAdminRequestHandler( + channel, + reprepareMessage, + repreparePayload.customPayload, + timeout, + throttler, + sessionMetricUpdater, + logPrefix, + "Reprepare " + reprepareMessage.toString()); + reprepareHandler + .start() + .handle( + (result, exception) -> { + if (exception != null) { + // If the error is not recoverable, surface it to the client instead of retrying + if (exception instanceof UnexpectedResponseException) { + Message prepareErrorMessage = + ((UnexpectedResponseException) exception).message; + if (prepareErrorMessage instanceof Error) { + CoordinatorException prepareError = + Conversions.toThrowable(node, (Error) prepareErrorMessage, context); + if (prepareError instanceof QueryValidationException + || prepareError instanceof FunctionFailureException + || prepareError instanceof ProtocolError) { + LOG.trace("[{}] Unrecoverable error on reprepare, rethrowing", logPrefix); + trackNodeError(node, prepareError, NANOTIME_NOT_MEASURED_YET); + setFinalError(prepareError, node, execution); + return null; + } + } + } else if (exception instanceof RequestThrottlingException) { + setFinalError(exception, node, execution); + return null; + } + recordError(node, exception); + trackNodeError(node, exception, NANOTIME_NOT_MEASURED_YET); + LOG.trace("[{}] Reprepare failed, trying next node", logPrefix); + sendRequest(null, queryPlan, execution, retryCount, false); + } else { + LOG.trace("[{}] Reprepare sucessful, retrying", logPrefix); + sendRequest(node, queryPlan, execution, retryCount, false); + } + return null; + }); + return; + } + CoordinatorException error = Conversions.toThrowable(node, errorMessage, context); + NodeMetricUpdater metricUpdater = ((DefaultNode) node).getMetricUpdater(); + if (error instanceof BootstrappingException) { + LOG.trace("[{}] {} is bootstrapping, trying next node", logPrefix, node); + recordError(node, error); + trackNodeError(node, error, NANOTIME_NOT_MEASURED_YET); + sendRequest(null, queryPlan, execution, retryCount, false); + } else if (error instanceof QueryValidationException + || error instanceof FunctionFailureException + || error instanceof ProtocolError) { + LOG.trace("[{}] Unrecoverable error, rethrowing", logPrefix); + metricUpdater.incrementCounter(DefaultNodeMetric.OTHER_ERRORS, executionProfile.getName()); + trackNodeError(node, error, NANOTIME_NOT_MEASURED_YET); + setFinalError(error, node, execution); + } else { + RetryDecision decision; + if (error instanceof ReadTimeoutException) { + ReadTimeoutException readTimeout = (ReadTimeoutException) error; + decision = + retryPolicy.onReadTimeout( + statement, + readTimeout.getConsistencyLevel(), + readTimeout.getBlockFor(), + readTimeout.getReceived(), + readTimeout.wasDataPresent(), + retryCount); + updateErrorMetrics( + metricUpdater, + decision, + DefaultNodeMetric.READ_TIMEOUTS, + DefaultNodeMetric.RETRIES_ON_READ_TIMEOUT, + DefaultNodeMetric.IGNORES_ON_READ_TIMEOUT); + } else if (error instanceof WriteTimeoutException) { + WriteTimeoutException writeTimeout = (WriteTimeoutException) error; + decision = + isIdempotent + ? retryPolicy.onWriteTimeout( + statement, + writeTimeout.getConsistencyLevel(), + writeTimeout.getWriteType(), + writeTimeout.getBlockFor(), + writeTimeout.getReceived(), + retryCount) + : RetryDecision.RETHROW; + updateErrorMetrics( + metricUpdater, + decision, + DefaultNodeMetric.WRITE_TIMEOUTS, + DefaultNodeMetric.RETRIES_ON_WRITE_TIMEOUT, + DefaultNodeMetric.IGNORES_ON_WRITE_TIMEOUT); + } else if (error instanceof UnavailableException) { + UnavailableException unavailable = (UnavailableException) error; + decision = + retryPolicy.onUnavailable( + statement, + unavailable.getConsistencyLevel(), + unavailable.getRequired(), + unavailable.getAlive(), + retryCount); + updateErrorMetrics( + metricUpdater, + decision, + DefaultNodeMetric.UNAVAILABLES, + DefaultNodeMetric.RETRIES_ON_UNAVAILABLE, + DefaultNodeMetric.IGNORES_ON_UNAVAILABLE); + } else { + decision = + isIdempotent + ? retryPolicy.onErrorResponse(statement, error, retryCount) + : RetryDecision.RETHROW; + updateErrorMetrics( + metricUpdater, + decision, + DefaultNodeMetric.OTHER_ERRORS, + DefaultNodeMetric.RETRIES_ON_OTHER_ERROR, + DefaultNodeMetric.IGNORES_ON_OTHER_ERROR); + } + processRetryDecision(decision, error); + } + } + + private void processRetryDecision(RetryDecision decision, Throwable error) { + LOG.trace("[{}] Processing retry decision {}", logPrefix, decision); + switch (decision) { + case RETRY_SAME: + recordError(node, error); + trackNodeError(node, error, NANOTIME_NOT_MEASURED_YET); + sendRequest(node, queryPlan, execution, retryCount + 1, false); + break; + case RETRY_NEXT: + recordError(node, error); + trackNodeError(node, error, NANOTIME_NOT_MEASURED_YET); + sendRequest(null, queryPlan, execution, retryCount + 1, false); + break; + case RETHROW: + trackNodeError(node, error, NANOTIME_NOT_MEASURED_YET); + setFinalError(error, node, execution); + break; + case IGNORE: + setFinalResult(Void.INSTANCE, null, true, this); + break; + } + } + + private void updateErrorMetrics( + NodeMetricUpdater metricUpdater, + RetryDecision decision, + DefaultNodeMetric error, + DefaultNodeMetric retriesOnError, + DefaultNodeMetric ignoresOnError) { + metricUpdater.incrementCounter(error, executionProfile.getName()); + switch (decision) { + case RETRY_SAME: + case RETRY_NEXT: + metricUpdater.incrementCounter(DefaultNodeMetric.RETRIES, executionProfile.getName()); + metricUpdater.incrementCounter(retriesOnError, executionProfile.getName()); + break; + case IGNORE: + metricUpdater.incrementCounter(DefaultNodeMetric.IGNORES, executionProfile.getName()); + metricUpdater.incrementCounter(ignoresOnError, executionProfile.getName()); + break; + case RETHROW: + // nothing do do + } + } + + @Override + public void onFailure(Throwable error) { + inFlightCallbacks.remove(this); + if (result.isDone()) { + return; + } + LOG.trace("[{}] Request failure, processing: {}", logPrefix, error.toString()); + RetryDecision decision; + if (!isIdempotent || error instanceof FrameTooLongException) { + decision = RetryDecision.RETHROW; + } else { + decision = retryPolicy.onRequestAborted(statement, error, retryCount); + } + processRetryDecision(decision, error); + updateErrorMetrics( + ((DefaultNode) node).getMetricUpdater(), + decision, + DefaultNodeMetric.ABORTED_REQUESTS, + DefaultNodeMetric.RETRIES_ON_ABORTED, + DefaultNodeMetric.IGNORES_ON_ABORTED); + } + + public void cancel() { + try { + if (!channel.closeFuture().isDone()) { + this.channel.cancel(this); + } + } catch (Throwable t) { + Loggers.warnWithException(LOG, "[{}] Error cancelling", logPrefix, t); + } + } + + /** + * @param nodeResponseTimeNanos the time we received the response, if it's already been + * measured. If {@link #NANOTIME_NOT_MEASURED_YET}, it hasn't and we need to measure it now + * (this is to avoid unnecessary calls to System.nanoTime) + */ + private void trackNodeError(Node node, Throwable error, long nodeResponseTimeNanos) { + if (requestTracker instanceof NoopRequestTracker) { + return; + } + if (nodeResponseTimeNanos == NANOTIME_NOT_MEASURED_YET) { + nodeResponseTimeNanos = System.nanoTime(); + } + long latencyNanos = nodeResponseTimeNanos - this.nodeStartTimeNanos; + requestTracker.onNodeError(statement, error, latencyNanos, executionProfile, node); + } + + @Override + public String toString() { + return logPrefix; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlRequestSyncProcessor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlRequestSyncProcessor.java new file mode 100644 index 00000000000..53cddc7772b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlRequestSyncProcessor.java @@ -0,0 +1,62 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.internal.core.session.RequestProcessor; +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class CqlRequestSyncProcessor implements RequestProcessor, ResultSet> { + + private final CqlRequestAsyncProcessor asyncProcessor; + + public CqlRequestSyncProcessor(CqlRequestAsyncProcessor asyncProcessor) { + this.asyncProcessor = asyncProcessor; + } + + @Override + public boolean canProcess(Request request, GenericType resultType) { + return request instanceof Statement && resultType.equals(Statement.SYNC); + } + + @Override + public ResultSet process( + Statement request, + DefaultSession session, + InternalDriverContext context, + String sessionLogPrefix) { + + BlockingOperation.checkNotDriverThread(); + AsyncResultSet firstPage = + CompletableFutures.getUninterruptibly( + asyncProcessor.process(request, session, context, sessionLogPrefix)); + return ResultSets.newInstance(firstPage); + } + + @Override + public ResultSet newFailure(RuntimeException error) { + throw error; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultAsyncResultSet.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultAsyncResultSet.java new file mode 100644 index 00000000000..b2630006b9a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultAsyncResultSet.java @@ -0,0 +1,170 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.util.CountingIterator; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletionStage; +import net.jcip.annotations.NotThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@NotThreadSafe // wraps a mutable queue +public class DefaultAsyncResultSet implements AsyncResultSet { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultAsyncResultSet.class); + + private final ColumnDefinitions definitions; + private final ExecutionInfo executionInfo; + private final CqlSession session; + private final CountingIterator iterator; + private final Iterable currentPage; + + public DefaultAsyncResultSet( + ColumnDefinitions definitions, + ExecutionInfo executionInfo, + Queue> data, + CqlSession session, + InternalDriverContext context) { + this.definitions = definitions; + this.executionInfo = executionInfo; + this.session = session; + this.iterator = + new CountingIterator(data.size()) { + @Override + protected Row computeNext() { + List rowData = data.poll(); + return (rowData == null) ? endOfData() : new DefaultRow(definitions, rowData, context); + } + }; + this.currentPage = () -> iterator; + } + + @NonNull + @Override + public ColumnDefinitions getColumnDefinitions() { + return definitions; + } + + @NonNull + @Override + public ExecutionInfo getExecutionInfo() { + return executionInfo; + } + + @NonNull + @Override + public Iterable currentPage() { + return currentPage; + } + + @Override + public int remaining() { + return iterator.remaining(); + } + + @Override + public boolean hasMorePages() { + return executionInfo.getPagingState() != null; + } + + @NonNull + @Override + public CompletionStage fetchNextPage() throws IllegalStateException { + ByteBuffer nextState = executionInfo.getPagingState(); + if (nextState == null) { + throw new IllegalStateException( + "No next page. Use #hasMorePages before calling this method to avoid this error."); + } + Statement statement = executionInfo.getStatement(); + LOG.trace("Fetching next page for {}", statement); + Statement nextStatement = statement.copy(nextState); + return session.executeAsync(nextStatement); + } + + @Override + public boolean wasApplied() { + if (!definitions.contains("[applied]") + || !definitions.get("[applied]").getType().equals(DataTypes.BOOLEAN)) { + return true; + } else if (iterator.hasNext()) { + // Note that [applied] has the same value for all rows, so as long as we have a row we don't + // care which one it is. + return iterator.peek().getBoolean("[applied]"); + } else { + // If the server provided [applied], it means there was at least one row. So if we get here it + // means the client consumed all the rows before, we can't handle that case because we have + // nowhere left to read the boolean from. + throw new IllegalStateException("This method must be called before consuming all the rows"); + } + } + + static AsyncResultSet empty(final ExecutionInfo executionInfo) { + return new AsyncResultSet() { + @NonNull + @Override + public ColumnDefinitions getColumnDefinitions() { + return EmptyColumnDefinitions.INSTANCE; + } + + @NonNull + @Override + public ExecutionInfo getExecutionInfo() { + return executionInfo; + } + + @NonNull + @Override + public Iterable currentPage() { + return Collections.emptyList(); + } + + @Override + public int remaining() { + return 0; + } + + @Override + public boolean hasMorePages() { + return false; + } + + @NonNull + @Override + public CompletionStage fetchNextPage() throws IllegalStateException { + throw new IllegalStateException( + "No next page. Use #hasMorePages before calling this method to avoid this error."); + } + + @Override + public boolean wasApplied() { + return true; + } + }; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultBatchStatement.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultBatchStatement.java new file mode 100644 index 00000000000..ad9fdbc0913 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultBatchStatement.java @@ -0,0 +1,733 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BatchType; +import com.datastax.oss.driver.api.core.cql.BatchableStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterables; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultBatchStatement implements BatchStatement { + + private final BatchType batchType; + private final List> statements; + private final String executionProfileName; + private final DriverExecutionProfile executionProfile; + private final CqlIdentifier keyspace; + private final CqlIdentifier routingKeyspace; + private final ByteBuffer routingKey; + private final Token routingToken; + private final Map customPayload; + private final Boolean idempotent; + private final boolean tracing; + private final long timestamp; + private final ByteBuffer pagingState; + private final int pageSize; + private final ConsistencyLevel consistencyLevel; + private final ConsistencyLevel serialConsistencyLevel; + private final Duration timeout; + private final Node node; + + public DefaultBatchStatement( + BatchType batchType, + List> statements, + String executionProfileName, + DriverExecutionProfile executionProfile, + CqlIdentifier keyspace, + CqlIdentifier routingKeyspace, + ByteBuffer routingKey, + Token routingToken, + Map customPayload, + Boolean idempotent, + boolean tracing, + long timestamp, + ByteBuffer pagingState, + int pageSize, + ConsistencyLevel consistencyLevel, + ConsistencyLevel serialConsistencyLevel, + Duration timeout, + Node node) { + this.batchType = batchType; + this.statements = ImmutableList.copyOf(statements); + this.executionProfileName = executionProfileName; + this.executionProfile = executionProfile; + this.keyspace = keyspace; + this.routingKeyspace = routingKeyspace; + this.routingKey = routingKey; + this.routingToken = routingToken; + this.customPayload = customPayload; + this.idempotent = idempotent; + this.tracing = tracing; + this.timestamp = timestamp; + this.pagingState = pagingState; + this.pageSize = pageSize; + this.consistencyLevel = consistencyLevel; + this.serialConsistencyLevel = serialConsistencyLevel; + this.timeout = timeout; + this.node = node; + } + + @NonNull + @Override + public BatchType getBatchType() { + return batchType; + } + + @NonNull + @Override + public BatchStatement setBatchType(@NonNull BatchType newBatchType) { + return new DefaultBatchStatement( + newBatchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @NonNull + @Override + public BatchStatement setKeyspace(@Nullable CqlIdentifier newKeyspace) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + newKeyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @NonNull + @Override + public BatchStatement add(@NonNull BatchableStatement statement) { + if (statements.size() >= 0xFFFF) { + throw new IllegalStateException( + "Batch statement cannot contain more than " + 0xFFFF + " statements."); + } else { + return new DefaultBatchStatement( + batchType, + ImmutableList.>builder().addAll(statements).add(statement).build(), + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + } + + @NonNull + @Override + public BatchStatement addAll(@NonNull Iterable> newStatements) { + if (statements.size() + Iterables.size(newStatements) > 0xFFFF) { + throw new IllegalStateException( + "Batch statement cannot contain more than " + 0xFFFF + " statements."); + } else { + return new DefaultBatchStatement( + batchType, + ImmutableList.>builder() + .addAll(statements) + .addAll(newStatements) + .build(), + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + } + + @Override + public int size() { + return statements.size(); + } + + @NonNull + @Override + public BatchStatement clear() { + return new DefaultBatchStatement( + batchType, + ImmutableList.of(), + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @NonNull + @Override + public Iterator> iterator() { + return statements.iterator(); + } + + @Override + public ByteBuffer getPagingState() { + return pagingState; + } + + @NonNull + @Override + public BatchStatement setPagingState(ByteBuffer newPagingState) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + newPagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Override + public int getPageSize() { + return pageSize; + } + + @NonNull + @Override + public BatchStatement setPageSize(int newPageSize) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + newPageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public ConsistencyLevel getConsistencyLevel() { + return consistencyLevel; + } + + @NonNull + @Override + public BatchStatement setConsistencyLevel(@Nullable ConsistencyLevel newConsistencyLevel) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + newConsistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public ConsistencyLevel getSerialConsistencyLevel() { + return serialConsistencyLevel; + } + + @NonNull + @Override + public BatchStatement setSerialConsistencyLevel( + @Nullable ConsistencyLevel newSerialConsistencyLevel) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + newSerialConsistencyLevel, + timeout, + node); + } + + @Override + public String getExecutionProfileName() { + return executionProfileName; + } + + @NonNull + @Override + public BatchStatement setExecutionProfileName(@Nullable String newConfigProfileName) { + return new DefaultBatchStatement( + batchType, + statements, + newConfigProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Override + public DriverExecutionProfile getExecutionProfile() { + return executionProfile; + } + + @NonNull + @Override + public DefaultBatchStatement setExecutionProfile(@Nullable DriverExecutionProfile newProfile) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + newProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Override + public CqlIdentifier getKeyspace() { + if (keyspace != null) { + return keyspace; + } else { + for (BatchableStatement statement : statements) { + if (statement instanceof SimpleStatement && statement.getKeyspace() != null) { + return statement.getKeyspace(); + } + } + } + return null; + } + + @Override + public CqlIdentifier getRoutingKeyspace() { + if (routingKeyspace != null) { + return routingKeyspace; + } else { + for (BatchableStatement statement : statements) { + CqlIdentifier ks = statement.getRoutingKeyspace(); + if (ks != null) { + return ks; + } + } + } + return null; + } + + @NonNull + @Override + public BatchStatement setRoutingKeyspace(CqlIdentifier newRoutingKeyspace) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + newRoutingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @NonNull + @Override + public BatchStatement setNode(@Nullable Node newNode) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + newNode); + } + + @Nullable + @Override + public Node getNode() { + return node; + } + + @Override + public ByteBuffer getRoutingKey() { + if (routingKey != null) { + return routingKey; + } else { + for (BatchableStatement statement : statements) { + ByteBuffer key = statement.getRoutingKey(); + if (key != null) { + return key; + } + } + } + return null; + } + + @NonNull + @Override + public BatchStatement setRoutingKey(ByteBuffer newRoutingKey) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + newRoutingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Override + public Token getRoutingToken() { + if (routingToken != null) { + return routingToken; + } else { + for (BatchableStatement statement : statements) { + Token token = statement.getRoutingToken(); + if (token != null) { + return token; + } + } + } + return null; + } + + @NonNull + @Override + public BatchStatement setRoutingToken(Token newRoutingToken) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + newRoutingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @NonNull + @Override + public Map getCustomPayload() { + return customPayload; + } + + @NonNull + @Override + public DefaultBatchStatement setCustomPayload(@NonNull Map newCustomPayload) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + newCustomPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Override + public Boolean isIdempotent() { + return idempotent; + } + + @Nullable + @Override + public Duration getTimeout() { + return null; + } + + @NonNull + @Override + public DefaultBatchStatement setIdempotent(Boolean newIdempotence) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + newIdempotence, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Override + public boolean isTracing() { + return tracing; + } + + @NonNull + @Override + public BatchStatement setTracing(boolean newTracing) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + newTracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Override + public long getQueryTimestamp() { + return timestamp; + } + + @NonNull + @Override + public BatchStatement setQueryTimestamp(long newTimestamp) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + newTimestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @NonNull + @Override + public BatchStatement setTimeout(@Nullable Duration newTimeout) { + return new DefaultBatchStatement( + batchType, + statements, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + newTimeout, + node); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultBoundStatement.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultBoundStatement.java new file mode 100644 index 00000000000..b0842670f06 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultBoundStatement.java @@ -0,0 +1,690 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.util.RoutingKey; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultBoundStatement implements BoundStatement { + + private final PreparedStatement preparedStatement; + private final ColumnDefinitions variableDefinitions; + private final ByteBuffer[] values; + private final String executionProfileName; + private final DriverExecutionProfile executionProfile; + private final CqlIdentifier routingKeyspace; + private final ByteBuffer routingKey; + private final Token routingToken; + private final Map customPayload; + private final Boolean idempotent; + private final boolean tracing; + private final long timestamp; + private final ByteBuffer pagingState; + private final int pageSize; + private final ConsistencyLevel consistencyLevel; + private final ConsistencyLevel serialConsistencyLevel; + private final Duration timeout; + private final CodecRegistry codecRegistry; + private final ProtocolVersion protocolVersion; + private final Node node; + + public DefaultBoundStatement( + PreparedStatement preparedStatement, + ColumnDefinitions variableDefinitions, + ByteBuffer[] values, + String executionProfileName, + DriverExecutionProfile executionProfile, + CqlIdentifier routingKeyspace, + ByteBuffer routingKey, + Token routingToken, + Map customPayload, + Boolean idempotent, + boolean tracing, + long timestamp, + ByteBuffer pagingState, + int pageSize, + ConsistencyLevel consistencyLevel, + ConsistencyLevel serialConsistencyLevel, + Duration timeout, + CodecRegistry codecRegistry, + ProtocolVersion protocolVersion, + Node node) { + this.preparedStatement = preparedStatement; + this.variableDefinitions = variableDefinitions; + this.values = values; + this.executionProfileName = executionProfileName; + this.executionProfile = executionProfile; + this.routingKeyspace = routingKeyspace; + this.routingKey = routingKey; + this.routingToken = routingToken; + this.customPayload = customPayload; + this.idempotent = idempotent; + this.tracing = tracing; + this.timestamp = timestamp; + this.pagingState = pagingState; + this.pageSize = pageSize; + this.consistencyLevel = consistencyLevel; + this.serialConsistencyLevel = serialConsistencyLevel; + this.timeout = timeout; + this.codecRegistry = codecRegistry; + this.protocolVersion = protocolVersion; + this.node = node; + } + + @Override + public int size() { + return variableDefinitions.size(); + } + + @NonNull + @Override + public DataType getType(int i) { + return variableDefinitions.get(i).getType(); + } + + @Override + public int firstIndexOf(@NonNull CqlIdentifier id) { + int indexOf = variableDefinitions.firstIndexOf(id); + if (indexOf == -1) { + throw new IllegalArgumentException(id + " is not a variable in this bound statement"); + } + return indexOf; + } + + @Override + public int firstIndexOf(@NonNull String name) { + int indexOf = variableDefinitions.firstIndexOf(name); + if (indexOf == -1) { + throw new IllegalArgumentException(name + " is not a variable in this bound statement"); + } + return indexOf; + } + + @NonNull + @Override + public CodecRegistry codecRegistry() { + return codecRegistry; + } + + @NonNull + @Override + public ProtocolVersion protocolVersion() { + return protocolVersion; + } + + @Override + public ByteBuffer getBytesUnsafe(int i) { + return values[i]; + } + + @NonNull + @Override + public BoundStatement setBytesUnsafe(int i, ByteBuffer v) { + ByteBuffer[] newValues = Arrays.copyOf(values, values.length); + newValues[i] = v; + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + newValues, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @NonNull + @Override + public PreparedStatement getPreparedStatement() { + return preparedStatement; + } + + @NonNull + @Override + public List getValues() { + return Arrays.asList(values); + } + + @Override + public String getExecutionProfileName() { + return executionProfileName; + } + + @NonNull + @Override + public BoundStatement setExecutionProfileName(@Nullable String newConfigProfileName) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + newConfigProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @Override + public DriverExecutionProfile getExecutionProfile() { + return executionProfile; + } + + @NonNull + @Override + public BoundStatement setExecutionProfile(@Nullable DriverExecutionProfile newProfile) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + newProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @Override + public CqlIdentifier getRoutingKeyspace() { + // If it was set explicitly, use that value, else try to infer it from the prepared statement's + // metadata + if (routingKeyspace != null) { + return routingKeyspace; + } else { + ColumnDefinitions definitions = preparedStatement.getResultSetDefinitions(); + return (definitions.size() == 0) ? null : definitions.get(0).getKeyspace(); + } + } + + @NonNull + @Override + public BoundStatement setRoutingKeyspace(@Nullable CqlIdentifier newRoutingKeyspace) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + newRoutingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @NonNull + @Override + public BoundStatement setNode(@Nullable Node newNode) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + newNode); + } + + @Nullable + @Override + public Node getNode() { + return node; + } + + @Override + public ByteBuffer getRoutingKey() { + if (routingKey != null) { + return routingKey; + } else { + List indices = preparedStatement.getPartitionKeyIndices(); + if (indices.isEmpty()) { + return null; + } else if (indices.size() == 1) { + return getBytesUnsafe(indices.get(0)); + } else { + ByteBuffer[] components = new ByteBuffer[indices.size()]; + for (int i = 0; i < components.length; i++) { + ByteBuffer value; + int index = indices.get(i); + if (!isSet(index) || (value = getBytesUnsafe(index)) == null) { + return null; + } else { + components[i] = value; + } + } + return RoutingKey.compose(components); + } + } + } + + @NonNull + @Override + public BoundStatement setRoutingKey(@Nullable ByteBuffer newRoutingKey) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + newRoutingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @Override + public Token getRoutingToken() { + return routingToken; + } + + @NonNull + @Override + public BoundStatement setRoutingToken(@Nullable Token newRoutingToken) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + newRoutingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @NonNull + @Override + public Map getCustomPayload() { + return customPayload; + } + + @NonNull + @Override + public BoundStatement setCustomPayload(@NonNull Map newCustomPayload) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + newCustomPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @Override + public Boolean isIdempotent() { + return idempotent; + } + + @NonNull + @Override + public BoundStatement setIdempotent(@Nullable Boolean newIdempotence) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + newIdempotence, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @Override + public boolean isTracing() { + return tracing; + } + + @NonNull + @Override + public BoundStatement setTracing(boolean newTracing) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + newTracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @Override + public long getQueryTimestamp() { + return timestamp; + } + + @NonNull + @Override + public BoundStatement setQueryTimestamp(long newTimestamp) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + newTimestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @Nullable + @Override + public Duration getTimeout() { + return timeout; + } + + @NonNull + @Override + public BoundStatement setTimeout(@Nullable Duration newTimeout) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + newTimeout, + codecRegistry, + protocolVersion, + node); + } + + @Override + public ByteBuffer getPagingState() { + return pagingState; + } + + @NonNull + @Override + public BoundStatement setPagingState(@Nullable ByteBuffer newPagingState) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + newPagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @Override + public int getPageSize() { + return pageSize; + } + + @NonNull + @Override + public BoundStatement setPageSize(int newPageSize) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + newPageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @Nullable + @Override + public ConsistencyLevel getConsistencyLevel() { + return consistencyLevel; + } + + @NonNull + @Override + public BoundStatement setConsistencyLevel(@Nullable ConsistencyLevel newConsistencyLevel) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + newConsistencyLevel, + serialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } + + @Nullable + @Override + public ConsistencyLevel getSerialConsistencyLevel() { + return serialConsistencyLevel; + } + + @NonNull + @Override + public BoundStatement setSerialConsistencyLevel( + @Nullable ConsistencyLevel newSerialConsistencyLevel) { + return new DefaultBoundStatement( + preparedStatement, + variableDefinitions, + values, + executionProfileName, + executionProfile, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + newSerialConsistencyLevel, + timeout, + codecRegistry, + protocolVersion, + node); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultColumnDefinition.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultColumnDefinition.java new file mode 100644 index 00000000000..94df9234eaa --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultColumnDefinition.java @@ -0,0 +1,89 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.internal.core.type.DataTypeHelper; +import com.datastax.oss.protocol.internal.response.result.ColumnSpec; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.Serializable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultColumnDefinition implements ColumnDefinition, Serializable { + + private static final long serialVersionUID = 1; + + /** @serial */ + private final CqlIdentifier keyspace; + /** @serial */ + private final CqlIdentifier table; + /** @serial */ + private final CqlIdentifier name; + /** @serial */ + private final DataType type; + + /** @param spec the raw data decoded by the protocol layer */ + public DefaultColumnDefinition( + @NonNull ColumnSpec spec, @NonNull AttachmentPoint attachmentPoint) { + this.keyspace = CqlIdentifier.fromInternal(spec.ksName); + this.table = CqlIdentifier.fromInternal(spec.tableName); + this.name = CqlIdentifier.fromInternal(spec.name); + this.type = DataTypeHelper.fromProtocolSpec(spec.type, attachmentPoint); + } + + @NonNull + @Override + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + @Override + public CqlIdentifier getTable() { + return table; + } + + @NonNull + @Override + public CqlIdentifier getName() { + return name; + } + + @NonNull + @Override + public DataType getType() { + return type; + } + + @Override + public boolean isDetached() { + return type.isDetached(); + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + type.attach(attachmentPoint); + } + + @Override + public String toString() { + return keyspace.asCql(true) + "." + table.asCql(true) + "." + name.asCql(true) + " " + type; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultColumnDefinitions.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultColumnDefinitions.java new file mode 100644 index 00000000000..74b345d79bb --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultColumnDefinitions.java @@ -0,0 +1,133 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.internal.core.data.IdentifierIndex; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultColumnDefinitions implements ColumnDefinitions, Serializable { + + public static ColumnDefinitions valueOf(List definitions) { + return definitions.isEmpty() + ? EmptyColumnDefinitions.INSTANCE + : new DefaultColumnDefinitions(definitions); + } + + private final List definitions; + private final IdentifierIndex index; + + private DefaultColumnDefinitions(List definitions) { + assert definitions != null && definitions.size() > 0; + this.definitions = definitions; + this.index = buildIndex(definitions); + } + + @Override + public int size() { + return definitions.size(); + } + + @NonNull + @Override + public ColumnDefinition get(int i) { + return definitions.get(i); + } + + @NonNull + @Override + public Iterator iterator() { + return definitions.iterator(); + } + + @Override + public boolean contains(@NonNull String name) { + return index.firstIndexOf(name) >= 0; + } + + @Override + public boolean contains(@NonNull CqlIdentifier id) { + return index.firstIndexOf(id) >= 0; + } + + @Override + public int firstIndexOf(@NonNull String name) { + return index.firstIndexOf(name); + } + + @Override + public int firstIndexOf(@NonNull CqlIdentifier id) { + return index.firstIndexOf(id); + } + + @Override + public boolean isDetached() { + return definitions.get(0).isDetached(); + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + for (ColumnDefinition definition : definitions) { + definition.attach(attachmentPoint); + } + } + + private static IdentifierIndex buildIndex(List definitions) { + List identifiers = new ArrayList<>(definitions.size()); + for (ColumnDefinition definition : definitions) { + identifiers.add(definition.getName()); + } + return new IdentifierIndex(identifiers); + } + + /** + * @serialData The list of definitions (the identifier index is reconstructed at deserialization). + */ + private Object writeReplace() { + return new SerializationProxy(this); + } + + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + // Should never be called since we serialized a proxy + throw new InvalidObjectException("Proxy required"); + } + + private static class SerializationProxy implements Serializable { + + private static final long serialVersionUID = 1; + + private final List definitions; + + private SerializationProxy(DefaultColumnDefinitions columnDefinitions) { + this.definitions = columnDefinitions.definitions; + } + + private Object readResolve() { + return new DefaultColumnDefinitions(this.definitions); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultExecutionInfo.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultExecutionInfo.java new file mode 100644 index 00000000000..bf542923405 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultExecutionInfo.java @@ -0,0 +1,166 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.QueryTrace; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.protocol.internal.Frame; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultExecutionInfo implements ExecutionInfo { + + private final Statement statement; + private final Node coordinator; + private final int speculativeExecutionCount; + private final int successfulExecutionIndex; + private final List> errors; + private final ByteBuffer pagingState; + private final UUID tracingId; + private final int responseSizeInBytes; + private final int compressedResponseSizeInBytes; + private final List warnings; + private final Map customPayload; + private final boolean schemaInAgreement; + private final DefaultSession session; + private final InternalDriverContext context; + private final DriverExecutionProfile executionProfile; + + public DefaultExecutionInfo( + Statement statement, + Node coordinator, + int speculativeExecutionCount, + int successfulExecutionIndex, + List> errors, + ByteBuffer pagingState, + Frame frame, + boolean schemaInAgreement, + DefaultSession session, + InternalDriverContext context, + DriverExecutionProfile executionProfile) { + this.statement = statement; + this.coordinator = coordinator; + this.speculativeExecutionCount = speculativeExecutionCount; + this.successfulExecutionIndex = successfulExecutionIndex; + this.errors = errors; + this.pagingState = pagingState; + + this.tracingId = (frame == null) ? null : frame.tracingId; + this.responseSizeInBytes = (frame == null) ? -1 : frame.size; + this.compressedResponseSizeInBytes = (frame == null) ? -1 : frame.compressedSize; + // Note: the collections returned by the protocol layer are already unmodifiable + this.warnings = (frame == null) ? Collections.emptyList() : frame.warnings; + this.customPayload = (frame == null) ? Collections.emptyMap() : frame.customPayload; + this.schemaInAgreement = schemaInAgreement; + this.session = session; + this.context = context; + this.executionProfile = executionProfile; + } + + @NonNull + @Override + public Statement getStatement() { + return statement; + } + + @Nullable + @Override + public Node getCoordinator() { + return coordinator; + } + + @Override + public int getSpeculativeExecutionCount() { + return speculativeExecutionCount; + } + + @Override + public int getSuccessfulExecutionIndex() { + return successfulExecutionIndex; + } + + @NonNull + @Override + public List> getErrors() { + // Assume this method will be called 0 or 1 time, so we create the unmodifiable wrapper on + // demand. + return (errors == null) ? Collections.emptyList() : Collections.unmodifiableList(errors); + } + + @Override + @Nullable + public ByteBuffer getPagingState() { + return pagingState; + } + + @NonNull + @Override + public List getWarnings() { + return warnings; + } + + @NonNull + @Override + public Map getIncomingPayload() { + return customPayload; + } + + @Override + public boolean isSchemaInAgreement() { + return schemaInAgreement; + } + + @Override + @Nullable + public UUID getTracingId() { + return tracingId; + } + + @NonNull + @Override + public CompletionStage getQueryTraceAsync() { + if (tracingId == null) { + return CompletableFutures.failedFuture( + new IllegalStateException("Tracing was disabled for this request")); + } else { + return new QueryTraceFetcher(tracingId, session, context, executionProfile).fetch(); + } + } + + @Override + public int getResponseSizeInBytes() { + return responseSizeInBytes; + } + + @Override + public int getCompressedResponseSizeInBytes() { + return compressedResponseSizeInBytes; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultPrepareRequest.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultPrepareRequest.java new file mode 100644 index 00000000000..149a4cb3017 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultPrepareRequest.java @@ -0,0 +1,219 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.PrepareRequest; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Map; +import net.jcip.annotations.Immutable; + +/** + * Default implementation of a prepare request, which is built internally to handle calls such as + * {@link CqlSession#prepare(String)} and {@link CqlSession#prepare(SimpleStatement)}. + * + *

When built from a {@link SimpleStatement}, it propagates the attributes to bound statements + * according to the rules described in {@link CqlSession#prepare(SimpleStatement)}. The prepare + * request itself: + * + *

    + *
  • will use the same execution profile (or execution profile name) as the {@code + * SimpleStatement}; + *
  • will use the same custom payload as the {@code SimpleStatement}; + *
  • will use a {@code null} timeout in order to default to the configuration (assuming that if + * a statement with a custom timeout is prepared, it is intended for the bound statements, not + * the preparation itself). + *
+ */ +@Immutable +public class DefaultPrepareRequest implements PrepareRequest { + + private final SimpleStatement statement; + + public DefaultPrepareRequest(SimpleStatement statement) { + this.statement = statement; + } + + public DefaultPrepareRequest(String query) { + this.statement = SimpleStatement.newInstance(query); + } + + @NonNull + @Override + public String getQuery() { + return statement.getQuery(); + } + + @Nullable + @Override + public String getExecutionProfileName() { + return statement.getExecutionProfileName(); + } + + @Nullable + @Override + public DriverExecutionProfile getExecutionProfile() { + return statement.getExecutionProfile(); + } + + @Nullable + @Override + public CqlIdentifier getKeyspace() { + return statement.getKeyspace(); + } + + @Nullable + @Override + public CqlIdentifier getRoutingKeyspace() { + // Prepare requests do not operate on a particular partition, token-aware routing doesn't apply. + return null; + } + + @Nullable + @Override + public ByteBuffer getRoutingKey() { + return null; + } + + @Nullable + @Override + public Token getRoutingToken() { + return null; + } + + @NonNull + @Override + public Map getCustomPayload() { + return statement.getCustomPayload(); + } + + @Nullable + @Override + public Duration getTimeout() { + return null; + } + + @Nullable + @Override + public String getExecutionProfileNameForBoundStatements() { + return statement.getExecutionProfileName(); + } + + @Nullable + @Override + public DriverExecutionProfile getExecutionProfileForBoundStatements() { + return statement.getExecutionProfile(); + } + + @Nullable + @Override + public CqlIdentifier getRoutingKeyspaceForBoundStatements() { + return (statement.getKeyspace() != null) + ? statement.getKeyspace() + : statement.getRoutingKeyspace(); + } + + @Nullable + @Override + public ByteBuffer getRoutingKeyForBoundStatements() { + return statement.getRoutingKey(); + } + + @Nullable + @Override + public Token getRoutingTokenForBoundStatements() { + return statement.getRoutingToken(); + } + + @NonNull + @Override + public Map getCustomPayloadForBoundStatements() { + return statement.getCustomPayload(); + } + + @Nullable + @Override + public Boolean areBoundStatementsIdempotent() { + return statement.isIdempotent(); + } + + @Nullable + @Override + public Duration getTimeoutForBoundStatements() { + return statement.getTimeout(); + } + + @Nullable + @Override + public ByteBuffer getPagingStateForBoundStatements() { + return statement.getPagingState(); + } + + @Override + public int getPageSizeForBoundStatements() { + return statement.getPageSize(); + } + + @Nullable + @Override + public ConsistencyLevel getConsistencyLevelForBoundStatements() { + return statement.getConsistencyLevel(); + } + + @Nullable + @Override + public ConsistencyLevel getSerialConsistencyLevelForBoundStatements() { + return statement.getSerialConsistencyLevel(); + } + + @Nullable + @Override + public Node getNode() { + // never target prepare requests + return null; + } + + @Override + public boolean areBoundStatementsTracing() { + return statement.isTracing(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof DefaultPrepareRequest) { + DefaultPrepareRequest that = (DefaultPrepareRequest) other; + return this.statement.equals(that.statement); + } else { + return false; + } + } + + @Override + public int hashCode() { + return statement.hashCode(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultPreparedStatement.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultPreparedStatement.java new file mode 100644 index 00000000000..8dfadf9f5a3 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultPreparedStatement.java @@ -0,0 +1,218 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.data.ValuesHelper; +import com.datastax.oss.driver.internal.core.session.RepreparePayload; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DefaultPreparedStatement implements PreparedStatement { + + private final ByteBuffer id; + private final RepreparePayload repreparePayload; + private final ColumnDefinitions variableDefinitions; + private final List partitionKeyIndices; + private volatile ResultMetadata resultMetadata; + private final CodecRegistry codecRegistry; + private final ProtocolVersion protocolVersion; + private final String executionProfileNameForBoundStatements; + private final DriverExecutionProfile executionProfileForBoundStatements; + private final ByteBuffer pagingStateForBoundStatements; + private final CqlIdentifier routingKeyspaceForBoundStatements; + private final ByteBuffer routingKeyForBoundStatements; + private final Token routingTokenForBoundStatements; + private final Map customPayloadForBoundStatements; + private final Boolean areBoundStatementsIdempotent; + private final boolean areBoundStatementsTracing; + private final int pageSizeForBoundStatements; + private final ConsistencyLevel consistencyLevelForBoundStatements; + private final ConsistencyLevel serialConsistencyLevelForBoundStatements; + private final Duration timeoutForBoundStatements; + + public DefaultPreparedStatement( + ByteBuffer id, + String query, + ColumnDefinitions variableDefinitions, + List partitionKeyIndices, + ByteBuffer resultMetadataId, + ColumnDefinitions resultSetDefinitions, + CqlIdentifier keyspace, + Map customPayloadForPrepare, + String executionProfileNameForBoundStatements, + DriverExecutionProfile executionProfileForBoundStatements, + CqlIdentifier routingKeyspaceForBoundStatements, + ByteBuffer routingKeyForBoundStatements, + Token routingTokenForBoundStatements, + Map customPayloadForBoundStatements, + Boolean areBoundStatementsIdempotent, + Duration timeoutForBoundStatements, + ByteBuffer pagingStateForBoundStatements, + int pageSizeForBoundStatements, + ConsistencyLevel consistencyLevelForBoundStatements, + ConsistencyLevel serialConsistencyLevelForBoundStatements, + boolean areBoundStatementsTracing, + CodecRegistry codecRegistry, + ProtocolVersion protocolVersion) { + this.id = id; + this.partitionKeyIndices = partitionKeyIndices; + // It's important that we keep a reference to this object, so that it only gets evicted from + // the map in DefaultSession if no client reference the PreparedStatement anymore. + this.repreparePayload = new RepreparePayload(id, query, keyspace, customPayloadForPrepare); + this.variableDefinitions = variableDefinitions; + this.resultMetadata = new ResultMetadata(resultMetadataId, resultSetDefinitions); + + this.executionProfileNameForBoundStatements = executionProfileNameForBoundStatements; + this.executionProfileForBoundStatements = executionProfileForBoundStatements; + this.routingKeyspaceForBoundStatements = routingKeyspaceForBoundStatements; + this.routingKeyForBoundStatements = routingKeyForBoundStatements; + this.routingTokenForBoundStatements = routingTokenForBoundStatements; + this.customPayloadForBoundStatements = customPayloadForBoundStatements; + this.areBoundStatementsIdempotent = areBoundStatementsIdempotent; + this.timeoutForBoundStatements = timeoutForBoundStatements; + this.pagingStateForBoundStatements = pagingStateForBoundStatements; + this.pageSizeForBoundStatements = pageSizeForBoundStatements; + this.consistencyLevelForBoundStatements = consistencyLevelForBoundStatements; + this.serialConsistencyLevelForBoundStatements = serialConsistencyLevelForBoundStatements; + this.areBoundStatementsTracing = areBoundStatementsTracing; + + this.codecRegistry = codecRegistry; + this.protocolVersion = protocolVersion; + } + + @NonNull + @Override + public ByteBuffer getId() { + return id; + } + + @NonNull + @Override + public String getQuery() { + return repreparePayload.query; + } + + @NonNull + @Override + public ColumnDefinitions getVariableDefinitions() { + return variableDefinitions; + } + + @NonNull + @Override + public List getPartitionKeyIndices() { + return partitionKeyIndices; + } + + @Override + public ByteBuffer getResultMetadataId() { + return resultMetadata.resultMetadataId; + } + + @NonNull + @Override + public ColumnDefinitions getResultSetDefinitions() { + return resultMetadata.resultSetDefinitions; + } + + @Override + public void setResultMetadata( + @NonNull ByteBuffer newResultMetadataId, @NonNull ColumnDefinitions newResultSetDefinitions) { + this.resultMetadata = new ResultMetadata(newResultMetadataId, newResultSetDefinitions); + } + + @NonNull + @Override + public BoundStatement bind(@NonNull Object... values) { + return new DefaultBoundStatement( + this, + variableDefinitions, + ValuesHelper.encodePreparedValues( + values, variableDefinitions, codecRegistry, protocolVersion), + executionProfileNameForBoundStatements, + executionProfileForBoundStatements, + routingKeyspaceForBoundStatements, + routingKeyForBoundStatements, + routingTokenForBoundStatements, + customPayloadForBoundStatements, + areBoundStatementsIdempotent, + areBoundStatementsTracing, + Long.MIN_VALUE, + pagingStateForBoundStatements, + pageSizeForBoundStatements, + consistencyLevelForBoundStatements, + serialConsistencyLevelForBoundStatements, + timeoutForBoundStatements, + codecRegistry, + protocolVersion, + null); + } + + @NonNull + @Override + public BoundStatementBuilder boundStatementBuilder(@NonNull Object... values) { + return new BoundStatementBuilder( + this, + variableDefinitions, + ValuesHelper.encodePreparedValues( + values, variableDefinitions, codecRegistry, protocolVersion), + executionProfileNameForBoundStatements, + executionProfileForBoundStatements, + routingKeyspaceForBoundStatements, + routingKeyForBoundStatements, + routingTokenForBoundStatements, + customPayloadForBoundStatements, + areBoundStatementsIdempotent, + areBoundStatementsTracing, + Long.MIN_VALUE, + pagingStateForBoundStatements, + pageSizeForBoundStatements, + consistencyLevelForBoundStatements, + serialConsistencyLevelForBoundStatements, + timeoutForBoundStatements, + codecRegistry, + protocolVersion); + } + + public RepreparePayload getRepreparePayload() { + return this.repreparePayload; + } + + private static class ResultMetadata { + private ByteBuffer resultMetadataId; + private ColumnDefinitions resultSetDefinitions; + + private ResultMetadata(ByteBuffer resultMetadataId, ColumnDefinitions resultSetDefinitions) { + this.resultMetadataId = resultMetadataId; + this.resultSetDefinitions = resultSetDefinitions; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultQueryTrace.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultQueryTrace.java new file mode 100644 index 00000000000..1caaace911c --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultQueryTrace.java @@ -0,0 +1,99 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.cql.QueryTrace; +import com.datastax.oss.driver.api.core.cql.TraceEvent; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetAddress; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultQueryTrace implements QueryTrace { + + private final UUID tracingId; + private final String requestType; + private final int durationMicros; + private final InetAddress coordinator; + private final Map parameters; + private final long startedAt; + private final List events; + + public DefaultQueryTrace( + UUID tracingId, + String requestType, + int durationMicros, + InetAddress coordinator, + Map parameters, + long startedAt, + List events) { + this.tracingId = tracingId; + this.requestType = requestType; + this.durationMicros = durationMicros; + this.coordinator = coordinator; + this.parameters = parameters; + this.startedAt = startedAt; + this.events = events; + } + + @NonNull + @Override + public UUID getTracingId() { + return tracingId; + } + + @NonNull + @Override + public String getRequestType() { + return requestType; + } + + @Override + public int getDurationMicros() { + return durationMicros; + } + + @NonNull + @Override + public InetAddress getCoordinator() { + return coordinator; + } + + @NonNull + @Override + public Map getParameters() { + return parameters; + } + + @Override + public long getStartedAt() { + return startedAt; + } + + @NonNull + @Override + public List getEvents() { + return events; + } + + @Override + public String toString() { + return String.format("%s [%s] - %dµs", requestType, tracingId, durationMicros); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultRow.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultRow.java new file mode 100644 index 00000000000..1b4db7968f6 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultRow.java @@ -0,0 +1,167 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultRow implements Row, Serializable { + + private final ColumnDefinitions definitions; + private final List data; + private transient volatile AttachmentPoint attachmentPoint; + + public DefaultRow( + ColumnDefinitions definitions, List data, AttachmentPoint attachmentPoint) { + this.definitions = definitions; + this.data = data; + this.attachmentPoint = attachmentPoint; + } + + public DefaultRow(ColumnDefinitions definitions, List data) { + this(definitions, data, AttachmentPoint.NONE); + } + + @NonNull + @Override + public ColumnDefinitions getColumnDefinitions() { + return definitions; + } + + @Override + public int size() { + return definitions.size(); + } + + @NonNull + @Override + public DataType getType(int i) { + return definitions.get(i).getType(); + } + + @Override + public int firstIndexOf(@NonNull CqlIdentifier id) { + int indexOf = definitions.firstIndexOf(id); + if (indexOf == -1) { + throw new IllegalArgumentException(id + " is not a column in this row"); + } + return indexOf; + } + + @NonNull + @Override + public DataType getType(@NonNull CqlIdentifier id) { + return definitions.get(firstIndexOf(id)).getType(); + } + + @Override + public int firstIndexOf(@NonNull String name) { + int indexOf = definitions.firstIndexOf(name); + if (indexOf == -1) { + throw new IllegalArgumentException(name + " is not a column in this row"); + } + return indexOf; + } + + @NonNull + @Override + public DataType getType(@NonNull String name) { + return definitions.get(firstIndexOf(name)).getType(); + } + + @NonNull + @Override + public CodecRegistry codecRegistry() { + return attachmentPoint.getCodecRegistry(); + } + + @NonNull + @Override + public ProtocolVersion protocolVersion() { + return attachmentPoint.getProtocolVersion(); + } + + @Override + public boolean isDetached() { + return attachmentPoint == AttachmentPoint.NONE; + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + this.attachmentPoint = attachmentPoint; + this.definitions.attach(attachmentPoint); + } + + @Nullable + @Override + public ByteBuffer getBytesUnsafe(int i) { + return data.get(i); + } + /** + * @serialData The column definitions, followed by an array of byte arrays representing the column + * values (null values are represented by {@code null}). + */ + private Object writeReplace() { + return new SerializationProxy(this); + } + + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + // Should never be called since we serialized a proxy + throw new InvalidObjectException("Proxy required"); + } + + private static class SerializationProxy implements Serializable { + + private static final long serialVersionUID = 1; + + private final ColumnDefinitions definitions; + private final byte[][] values; + + SerializationProxy(DefaultRow row) { + this.definitions = row.definitions; + this.values = new byte[row.data.size()][]; + int i = 0; + for (ByteBuffer buffer : row.data) { + this.values[i] = (buffer == null) ? null : Bytes.getArray(buffer); + i += 1; + } + } + + private Object readResolve() { + List data = new ArrayList<>(this.values.length); + for (byte[] value : this.values) { + data.add((value == null) ? null : ByteBuffer.wrap(value)); + } + return new DefaultRow(this.definitions, data); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultSimpleStatement.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultSimpleStatement.java new file mode 100644 index 00000000000..acad2e11051 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultSimpleStatement.java @@ -0,0 +1,753 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableList; +import com.datastax.oss.protocol.internal.util.collection.NullAllowingImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultSimpleStatement implements SimpleStatement { + + private final String query; + private final List positionalValues; + private final Map namedValues; + private final String executionProfileName; + private final DriverExecutionProfile executionProfile; + private final CqlIdentifier keyspace; + private final CqlIdentifier routingKeyspace; + private final ByteBuffer routingKey; + private final Token routingToken; + + private final Map customPayload; + private final Boolean idempotent; + private final boolean tracing; + private final long timestamp; + private final ByteBuffer pagingState; + private final int pageSize; + private final ConsistencyLevel consistencyLevel; + private final ConsistencyLevel serialConsistencyLevel; + private final Duration timeout; + private final Node node; + + /** @see SimpleStatement#builder(String) */ + public DefaultSimpleStatement( + String query, + List positionalValues, + Map namedValues, + String executionProfileName, + DriverExecutionProfile executionProfile, + CqlIdentifier keyspace, + CqlIdentifier routingKeyspace, + ByteBuffer routingKey, + Token routingToken, + Map customPayload, + Boolean idempotent, + boolean tracing, + long timestamp, + ByteBuffer pagingState, + int pageSize, + ConsistencyLevel consistencyLevel, + ConsistencyLevel serialConsistencyLevel, + Duration timeout, + Node node) { + if (!positionalValues.isEmpty() && !namedValues.isEmpty()) { + throw new IllegalArgumentException("Can't have both positional and named values"); + } + this.query = query; + this.positionalValues = NullAllowingImmutableList.copyOf(positionalValues); + this.namedValues = NullAllowingImmutableMap.copyOf(namedValues); + this.executionProfileName = executionProfileName; + this.executionProfile = executionProfile; + this.keyspace = keyspace; + this.routingKeyspace = routingKeyspace; + this.routingKey = routingKey; + this.routingToken = routingToken; + this.customPayload = customPayload; + this.idempotent = idempotent; + this.tracing = tracing; + this.timestamp = timestamp; + this.pagingState = pagingState; + this.pageSize = pageSize; + this.consistencyLevel = consistencyLevel; + this.serialConsistencyLevel = serialConsistencyLevel; + this.timeout = timeout; + this.node = node; + } + + @NonNull + @Override + public String getQuery() { + return query; + } + + @NonNull + @Override + public SimpleStatement setQuery(@NonNull String newQuery) { + return new DefaultSimpleStatement( + newQuery, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @NonNull + @Override + public List getPositionalValues() { + return positionalValues; + } + + @NonNull + @Override + public SimpleStatement setPositionalValues(@NonNull List newPositionalValues) { + return new DefaultSimpleStatement( + query, + newPositionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @NonNull + @Override + public Map getNamedValues() { + return namedValues; + } + + @NonNull + @Override + public SimpleStatement setNamedValuesWithIds(@NonNull Map newNamedValues) { + return new DefaultSimpleStatement( + query, + positionalValues, + newNamedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public String getExecutionProfileName() { + return executionProfileName; + } + + @NonNull + @Override + public SimpleStatement setExecutionProfileName(@Nullable String newConfigProfileName) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + newConfigProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public DriverExecutionProfile getExecutionProfile() { + return executionProfile; + } + + @NonNull + @Override + public SimpleStatement setExecutionProfile(@Nullable DriverExecutionProfile newProfile) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + null, + newProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + @Override + public SimpleStatement setKeyspace(@Nullable CqlIdentifier newKeyspace) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + newKeyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public CqlIdentifier getRoutingKeyspace() { + return routingKeyspace; + } + + @NonNull + @Override + public SimpleStatement setRoutingKeyspace(@Nullable CqlIdentifier newRoutingKeyspace) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + newRoutingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @NonNull + @Override + public SimpleStatement setNode(@Nullable Node newNode) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + newNode); + } + + @Nullable + @Override + public Node getNode() { + return node; + } + + @Nullable + @Override + public ByteBuffer getRoutingKey() { + return routingKey; + } + + @NonNull + @Override + public SimpleStatement setRoutingKey(@Nullable ByteBuffer newRoutingKey) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + newRoutingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public Token getRoutingToken() { + return routingToken; + } + + @NonNull + @Override + public SimpleStatement setRoutingToken(@Nullable Token newRoutingToken) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + newRoutingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @NonNull + @Override + public Map getCustomPayload() { + return customPayload; + } + + @NonNull + @Override + public SimpleStatement setCustomPayload(@NonNull Map newCustomPayload) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + newCustomPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public Boolean isIdempotent() { + return idempotent; + } + + @NonNull + @Override + public SimpleStatement setIdempotent(@Nullable Boolean newIdempotence) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + newIdempotence, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Override + public boolean isTracing() { + return tracing; + } + + @NonNull + @Override + public SimpleStatement setTracing(boolean newTracing) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + newTracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Override + public long getQueryTimestamp() { + return timestamp; + } + + @NonNull + @Override + public SimpleStatement setQueryTimestamp(long newTimestamp) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + newTimestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public Duration getTimeout() { + return timeout; + } + + @NonNull + @Override + public SimpleStatement setTimeout(@Nullable Duration newTimeout) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + newTimeout, + node); + } + + @Nullable + @Override + public ByteBuffer getPagingState() { + return pagingState; + } + + @NonNull + @Override + public SimpleStatement setPagingState(@Nullable ByteBuffer newPagingState) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + newPagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Override + public int getPageSize() { + return pageSize; + } + + @NonNull + @Override + public SimpleStatement setPageSize(int newPageSize) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + newPageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public ConsistencyLevel getConsistencyLevel() { + return consistencyLevel; + } + + @NonNull + @Override + public SimpleStatement setConsistencyLevel(@Nullable ConsistencyLevel newConsistencyLevel) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + newConsistencyLevel, + serialConsistencyLevel, + timeout, + node); + } + + @Nullable + @Override + public ConsistencyLevel getSerialConsistencyLevel() { + return serialConsistencyLevel; + } + + @NonNull + @Override + public SimpleStatement setSerialConsistencyLevel( + @Nullable ConsistencyLevel newSerialConsistencyLevel) { + return new DefaultSimpleStatement( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + newSerialConsistencyLevel, + timeout, + node); + } + + public static Map wrapKeys(Map namedValues) { + NullAllowingImmutableMap.Builder builder = + NullAllowingImmutableMap.builder(); + for (Map.Entry entry : namedValues.entrySet()) { + builder.put(CqlIdentifier.fromCql(entry.getKey()), entry.getValue()); + } + return builder.build(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof DefaultSimpleStatement) { + DefaultSimpleStatement that = (DefaultSimpleStatement) other; + return this.query.equals(that.query) + && this.positionalValues.equals(that.positionalValues) + && this.namedValues.equals(that.namedValues) + && Objects.equals(this.executionProfileName, that.executionProfileName) + && Objects.equals(this.executionProfile, that.executionProfile) + && Objects.equals(this.keyspace, that.keyspace) + && Objects.equals(this.routingKeyspace, that.routingKeyspace) + && Objects.equals(this.routingKey, that.routingKey) + && Objects.equals(this.routingToken, that.routingToken) + && Objects.equals(this.customPayload, that.customPayload) + && Objects.equals(this.idempotent, that.idempotent) + && this.tracing == that.tracing + && this.timestamp == that.timestamp + && Objects.equals(this.pagingState, that.pagingState) + && this.pageSize == that.pageSize + && Objects.equals(this.consistencyLevel, that.consistencyLevel) + && Objects.equals(this.serialConsistencyLevel, that.serialConsistencyLevel) + && Objects.equals(this.timeout, that.timeout) + && Objects.equals(this.node, that.node); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash( + query, + positionalValues, + namedValues, + executionProfileName, + executionProfile, + keyspace, + routingKeyspace, + routingKey, + routingToken, + customPayload, + idempotent, + tracing, + timestamp, + pagingState, + pageSize, + consistencyLevel, + serialConsistencyLevel, + timeout, + node); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultTraceEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultTraceEvent.java new file mode 100644 index 00000000000..fab045bd588 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/DefaultTraceEvent.java @@ -0,0 +1,75 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.cql.TraceEvent; +import java.net.InetAddress; +import java.util.Date; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultTraceEvent implements TraceEvent { + + private final String activity; + private final long timestamp; + private final InetAddress source; + private final int sourceElapsedMicros; + private final String threadName; + + public DefaultTraceEvent( + String activity, + long timestamp, + InetAddress source, + int sourceElapsedMicros, + String threadName) { + this.activity = activity; + // Convert the UUID timestamp to an epoch timestamp + this.timestamp = (timestamp - 0x01b21dd213814000L) / 10000; + this.source = source; + this.sourceElapsedMicros = sourceElapsedMicros; + this.threadName = threadName; + } + + @Override + public String getActivity() { + return activity; + } + + @Override + public long getTimestamp() { + return timestamp; + } + + @Override + public InetAddress getSource() { + return source; + } + + @Override + public int getSourceElapsedMicros() { + return sourceElapsedMicros; + } + + @Override + public String getThreadName() { + return threadName; + } + + @Override + public String toString() { + return String.format("%s on %s[%s] at %s", activity, source, threadName, new Date(timestamp)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/EmptyColumnDefinitions.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/EmptyColumnDefinitions.java new file mode 100644 index 00000000000..fde195ad74a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/EmptyColumnDefinitions.java @@ -0,0 +1,76 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Collections; +import java.util.Iterator; + +/** + * The singleton that represents no column definitions (implemented as an enum which provides the + * serialization machinery for free). + */ +public enum EmptyColumnDefinitions implements ColumnDefinitions { + INSTANCE; + + @Override + public int size() { + return 0; + } + + @NonNull + @Override + public ColumnDefinition get(int i) { + throw new ArrayIndexOutOfBoundsException(); + } + + @Override + public boolean contains(@NonNull String name) { + return false; + } + + @Override + public boolean contains(@NonNull CqlIdentifier id) { + return false; + } + + @Override + public int firstIndexOf(@NonNull String name) { + return -1; + } + + @Override + public int firstIndexOf(@NonNull CqlIdentifier id) { + return -1; + } + + @Override + public boolean isDetached() { + return false; + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) {} + + @Override + public Iterator iterator() { + return Collections.emptyList().iterator(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/MultiPageResultSet.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/MultiPageResultSet.java new file mode 100644 index 00000000000..e80b442726d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/MultiPageResultSet.java @@ -0,0 +1,118 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.internal.core.util.CountingIterator; +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import net.jcip.annotations.NotThreadSafe; + +@NotThreadSafe +public class MultiPageResultSet implements ResultSet { + + private final RowIterator iterator; + private final List executionInfos = new ArrayList<>(); + private ColumnDefinitions columnDefinitions; + + public MultiPageResultSet(@NonNull AsyncResultSet firstPage) { + assert firstPage.hasMorePages(); + this.iterator = new RowIterator(firstPage); + this.executionInfos.add(firstPage.getExecutionInfo()); + this.columnDefinitions = firstPage.getColumnDefinitions(); + } + + @NonNull + @Override + public ColumnDefinitions getColumnDefinitions() { + return columnDefinitions; + } + + @NonNull + @Override + public List getExecutionInfos() { + return executionInfos; + } + + @Override + public boolean isFullyFetched() { + return iterator.isFullyFetched(); + } + + @Override + public int getAvailableWithoutFetching() { + return iterator.remaining(); + } + + @NonNull + @Override + public Iterator iterator() { + return iterator; + } + + @Override + public boolean wasApplied() { + return iterator.wasApplied(); + } + + private class RowIterator extends CountingIterator { + private AsyncResultSet currentPage; + private Iterator currentRows; + + private RowIterator(AsyncResultSet firstPage) { + super(firstPage.remaining()); + this.currentPage = firstPage; + this.currentRows = firstPage.currentPage().iterator(); + } + + @Override + protected Row computeNext() { + maybeMoveToNextPage(); + return currentRows.hasNext() ? currentRows.next() : endOfData(); + } + + private void maybeMoveToNextPage() { + if (!currentRows.hasNext() && currentPage.hasMorePages()) { + BlockingOperation.checkNotDriverThread(); + AsyncResultSet nextPage = + CompletableFutures.getUninterruptibly(currentPage.fetchNextPage()); + currentPage = nextPage; + remaining += nextPage.remaining(); + currentRows = nextPage.currentPage().iterator(); + executionInfos.add(nextPage.getExecutionInfo()); + // The definitions can change from page to page if this result set was built from a bound + // 'SELECT *', and the schema was altered. + columnDefinitions = nextPage.getColumnDefinitions(); + } + } + + private boolean isFullyFetched() { + return !currentPage.hasMorePages(); + } + + private boolean wasApplied() { + return currentPage.wasApplied(); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/QueryTraceFetcher.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/QueryTraceFetcher.java new file mode 100644 index 00000000000..ebe7f906c25 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/QueryTraceFetcher.java @@ -0,0 +1,156 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.QueryTrace; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.TraceEvent; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterables; +import io.netty.util.concurrent.EventExecutor; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +class QueryTraceFetcher { + + private final UUID tracingId; + private final CqlSession session; + private final DriverExecutionProfile config; + private final int maxAttempts; + private final long intervalNanos; + private final EventExecutor scheduler; + private final CompletableFuture resultFuture = new CompletableFuture<>(); + + QueryTraceFetcher( + UUID tracingId, + CqlSession session, + InternalDriverContext context, + DriverExecutionProfile config) { + this.tracingId = tracingId; + this.session = session; + + String regularConsistency = config.getString(DefaultDriverOption.REQUEST_CONSISTENCY); + String traceConsistency = config.getString(DefaultDriverOption.REQUEST_TRACE_CONSISTENCY); + this.config = + (traceConsistency.equals(regularConsistency)) + ? config + : config.withString(DefaultDriverOption.REQUEST_CONSISTENCY, traceConsistency); + + this.maxAttempts = config.getInt(DefaultDriverOption.REQUEST_TRACE_ATTEMPTS); + this.intervalNanos = config.getDuration(DefaultDriverOption.REQUEST_TRACE_INTERVAL).toNanos(); + this.scheduler = context.getNettyOptions().adminEventExecutorGroup().next(); + + querySession(maxAttempts); + } + + CompletionStage fetch() { + return resultFuture; + } + + private void querySession(int remainingAttempts) { + session + .executeAsync( + SimpleStatement.builder("SELECT * FROM system_traces.sessions WHERE session_id = ?") + .addPositionalValue(tracingId) + .setExecutionProfile(config) + .build()) + .whenComplete( + (rs, error) -> { + if (error != null) { + resultFuture.completeExceptionally(error); + } else { + Row row = rs.one(); + if (row == null || row.isNull("duration") || row.isNull("started_at")) { + // Trace is incomplete => fail if last try, or schedule retry + if (remainingAttempts == 1) { + resultFuture.completeExceptionally( + new IllegalStateException( + String.format( + "Trace %s still not complete after %d attempts", + tracingId, maxAttempts))); + } else { + scheduler.schedule( + () -> querySession(remainingAttempts - 1), + intervalNanos, + TimeUnit.NANOSECONDS); + } + } else { + queryEvents(row, new ArrayList<>(), null); + } + } + }); + } + + private void queryEvents(Row sessionRow, List events, ByteBuffer pagingState) { + session + .executeAsync( + SimpleStatement.builder("SELECT * FROM system_traces.events WHERE session_id = ?") + .addPositionalValue(tracingId) + .setPagingState(pagingState) + .setExecutionProfile(config) + .build()) + .whenComplete( + (rs, error) -> { + if (error != null) { + resultFuture.completeExceptionally(error); + } else { + Iterables.addAll(events, rs.currentPage()); + ByteBuffer nextPagingState = rs.getExecutionInfo().getPagingState(); + if (nextPagingState == null) { + resultFuture.complete(buildTrace(sessionRow, events)); + } else { + queryEvents(sessionRow, events, nextPagingState); + } + } + }); + } + + private QueryTrace buildTrace(Row sessionRow, Iterable eventRows) { + ImmutableList.Builder eventsBuilder = ImmutableList.builder(); + for (Row eventRow : eventRows) { + UUID eventId = eventRow.getUuid("event_id"); + eventsBuilder.add( + new DefaultTraceEvent( + eventRow.getString("activity"), + eventId == null ? -1 : eventId.timestamp(), + eventRow.getInetAddress("source"), + eventRow.getInt("source_elapsed"), + eventRow.getString("thread"))); + } + Instant startedAt = sessionRow.getInstant("started_at"); + return new DefaultQueryTrace( + tracingId, + sessionRow.getString("request"), + sessionRow.getInt("duration"), + sessionRow.getInetAddress("coordinator"), + sessionRow.getMap("parameters", String.class, String.class), + startedAt == null ? -1 : startedAt.toEpochMilli(), + eventsBuilder.build()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/ResultSets.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/ResultSets.java new file mode 100644 index 00000000000..dfd5fc8def1 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/ResultSets.java @@ -0,0 +1,27 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ResultSet; + +public class ResultSets { + public static ResultSet newInstance(AsyncResultSet firstPage) { + return (firstPage.hasMorePages()) + ? new MultiPageResultSet(firstPage) + : new SinglePageResultSet(firstPage); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/cql/SinglePageResultSet.java b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/SinglePageResultSet.java new file mode 100644 index 00000000000..5d5e4047e42 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/cql/SinglePageResultSet.java @@ -0,0 +1,77 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Iterator; +import java.util.List; +import net.jcip.annotations.NotThreadSafe; + +@NotThreadSafe +public class SinglePageResultSet implements ResultSet { + private final AsyncResultSet onlyPage; + + public SinglePageResultSet(AsyncResultSet onlyPage) { + this.onlyPage = onlyPage; + assert !onlyPage.hasMorePages(); + } + + @NonNull + @Override + public ColumnDefinitions getColumnDefinitions() { + return onlyPage.getColumnDefinitions(); + } + + @NonNull + @Override + public ExecutionInfo getExecutionInfo() { + return onlyPage.getExecutionInfo(); + } + + @NonNull + @Override + public List getExecutionInfos() { + // Assuming this will be called 0 or 1 time, avoid creating the list if it's 0. + return ImmutableList.of(onlyPage.getExecutionInfo()); + } + + @Override + public boolean isFullyFetched() { + return true; + } + + @Override + public int getAvailableWithoutFetching() { + return onlyPage.remaining(); + } + + @NonNull + @Override + public Iterator iterator() { + return onlyPage.currentPage().iterator(); + } + + @Override + public boolean wasApplied() { + return onlyPage.wasApplied(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/data/DefaultTupleValue.java b/core/src/main/java/com/datastax/oss/driver/internal/core/data/DefaultTupleValue.java new file mode 100644 index 00000000000..9317a3f5a36 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/data/DefaultTupleValue.java @@ -0,0 +1,200 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.data; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultTupleValue implements TupleValue, Serializable { + + private static final long serialVersionUID = 1; + private final TupleType type; + private final ByteBuffer[] values; + + public DefaultTupleValue(@NonNull TupleType type) { + this(type, new ByteBuffer[type.getComponentTypes().size()]); + } + + public DefaultTupleValue(@NonNull TupleType type, @NonNull Object... values) { + this( + type, + ValuesHelper.encodeValues( + values, + type.getComponentTypes(), + type.getAttachmentPoint().getCodecRegistry(), + type.getAttachmentPoint().getProtocolVersion())); + } + + private DefaultTupleValue(TupleType type, ByteBuffer[] values) { + Preconditions.checkNotNull(type); + this.type = type; + this.values = values; + } + + @NonNull + @Override + public TupleType getType() { + return type; + } + + @Override + public int size() { + return values.length; + } + + @Override + public ByteBuffer getBytesUnsafe(int i) { + return values[i]; + } + + @NonNull + @Override + public TupleValue setBytesUnsafe(int i, @Nullable ByteBuffer v) { + values[i] = v; + return this; + } + + @NonNull + @Override + public DataType getType(int i) { + return type.getComponentTypes().get(i); + } + + @NonNull + @Override + public CodecRegistry codecRegistry() { + return type.getAttachmentPoint().getCodecRegistry(); + } + + @NonNull + @Override + public ProtocolVersion protocolVersion() { + return type.getAttachmentPoint().getProtocolVersion(); + } + + /** + * @serialData The type of the tuple, followed by an array of byte arrays representing the values + * (null values are represented by {@code null}). + */ + private Object writeReplace() { + return new SerializationProxy(this); + } + + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + // Should never be called since we serialized a proxy + throw new InvalidObjectException("Proxy required"); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof TupleValue)) { + return false; + } + TupleValue that = (TupleValue) o; + + if (!type.equals(that.getType())) { + return false; + } + + for (int i = 0; i < values.length; i++) { + DataType innerThisType = type.getComponentTypes().get(i); + DataType innerThatType = that.getType().getComponentTypes().get(i); + if (!innerThisType.equals(innerThatType)) { + return false; + } + Object thisValue = + this.codecRegistry() + .codecFor(innerThisType) + .decode(this.getBytesUnsafe(i), this.protocolVersion()); + Object thatValue = + that.codecRegistry() + .codecFor(innerThatType) + .decode(that.getBytesUnsafe(i), that.protocolVersion()); + if (!Objects.equals(thisValue, thatValue)) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + + int result = type.hashCode(); + + for (int i = 0; i < values.length; i++) { + DataType innerThisType = type.getComponentTypes().get(i); + Object thisValue = + this.codecRegistry() + .codecFor(innerThisType) + .decode(this.values[i], this.protocolVersion()); + if (thisValue != null) { + result = 31 * result + thisValue.hashCode(); + } + } + + return result; + } + + @Override + public String toString() { + return codecRegistry().codecFor(type).format(this); + } + + private static class SerializationProxy implements Serializable { + + private static final long serialVersionUID = 1; + + private final TupleType type; + private final byte[][] values; + + SerializationProxy(DefaultTupleValue tuple) { + this.type = tuple.type; + this.values = new byte[tuple.values.length][]; + for (int i = 0; i < tuple.values.length; i++) { + ByteBuffer buffer = tuple.values[i]; + this.values[i] = (buffer == null) ? null : Bytes.getArray(buffer); + } + } + + private Object readResolve() { + ByteBuffer[] buffers = new ByteBuffer[this.values.length]; + for (int i = 0; i < this.values.length; i++) { + byte[] value = this.values[i]; + buffers[i] = (value == null) ? null : ByteBuffer.wrap(value); + } + return new DefaultTupleValue(this.type, buffers); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/data/DefaultUdtValue.java b/core/src/main/java/com/datastax/oss/driver/internal/core/data/DefaultUdtValue.java new file mode 100644 index 00000000000..5bed077a76d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/data/DefaultUdtValue.java @@ -0,0 +1,217 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.data; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultUdtValue implements UdtValue, Serializable { + + private static final long serialVersionUID = 1; + + private final UserDefinedType type; + private final ByteBuffer[] values; + + public DefaultUdtValue(@NonNull UserDefinedType type) { + this(type, new ByteBuffer[type.getFieldTypes().size()]); + } + + public DefaultUdtValue(@NonNull UserDefinedType type, @NonNull Object... values) { + this( + type, + ValuesHelper.encodeValues( + values, + type.getFieldTypes(), + type.getAttachmentPoint().getCodecRegistry(), + type.getAttachmentPoint().getProtocolVersion())); + } + + private DefaultUdtValue(UserDefinedType type, ByteBuffer[] values) { + Preconditions.checkNotNull(type); + this.type = type; + this.values = values; + } + + @NonNull + @Override + public UserDefinedType getType() { + return type; + } + + @Override + public int size() { + return values.length; + } + + @Override + public int firstIndexOf(@NonNull CqlIdentifier id) { + int indexOf = type.firstIndexOf(id); + if (indexOf == -1) { + throw new IllegalArgumentException(id + " is not a field in this UDT"); + } + return indexOf; + } + + @Override + public int firstIndexOf(@NonNull String name) { + int indexOf = type.firstIndexOf(name); + if (indexOf == -1) { + throw new IllegalArgumentException(name + " is not a field in this UDT"); + } + return indexOf; + } + + @NonNull + @Override + public DataType getType(int i) { + return type.getFieldTypes().get(i); + } + + @Override + public ByteBuffer getBytesUnsafe(int i) { + return values[i]; + } + + @NonNull + @Override + public UdtValue setBytesUnsafe(int i, @Nullable ByteBuffer v) { + values[i] = v; + return this; + } + + @NonNull + @Override + public CodecRegistry codecRegistry() { + return type.getAttachmentPoint().getCodecRegistry(); + } + + @NonNull + @Override + public ProtocolVersion protocolVersion() { + return type.getAttachmentPoint().getProtocolVersion(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof UdtValue)) { + return false; + } + UdtValue that = (UdtValue) o; + + if (!type.equals(that.getType())) { + return false; + } + + for (int i = 0; i < values.length; i++) { + + DataType innerThisType = type.getFieldTypes().get(i); + DataType innerThatType = that.getType().getFieldTypes().get(i); + + Object thisValue = + this.codecRegistry() + .codecFor(innerThisType) + .decode(this.getBytesUnsafe(i), this.protocolVersion()); + Object thatValue = + that.codecRegistry() + .codecFor(innerThatType) + .decode(that.getBytesUnsafe(i), that.protocolVersion()); + + if (!Objects.equals(thisValue, thatValue)) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + int result = type.hashCode(); + for (int i = 0; i < values.length; i++) { + DataType innerThisType = type.getFieldTypes().get(i); + Object thisValue = + this.codecRegistry() + .codecFor(innerThisType) + .decode(this.values[i], this.protocolVersion()); + if (thisValue != null) { + result = 31 * result + thisValue.hashCode(); + } + } + return result; + } + + @Override + public String toString() { + return codecRegistry().codecFor(type).format(this); + } + + /** + * @serialData The type of the tuple, followed by an array of byte arrays representing the values + * (null values are represented by {@code null}). + */ + private Object writeReplace() { + return new SerializationProxy(this); + } + + private void readObject(ObjectInputStream stream) throws InvalidObjectException { + // Should never be called since we serialized a proxy + throw new InvalidObjectException("Proxy required"); + } + + private static class SerializationProxy implements Serializable { + + private static final long serialVersionUID = 1; + + private final UserDefinedType type; + private final byte[][] values; + + SerializationProxy(DefaultUdtValue udt) { + this.type = udt.type; + this.values = new byte[udt.values.length][]; + for (int i = 0; i < udt.values.length; i++) { + ByteBuffer buffer = udt.values[i]; + this.values[i] = (buffer == null) ? null : Bytes.getArray(buffer); + } + } + + private Object readResolve() { + ByteBuffer[] buffers = new ByteBuffer[this.values.length]; + for (int i = 0; i < this.values.length; i++) { + byte[] value = this.values[i]; + buffers[i] = (value == null) ? null : ByteBuffer.wrap(value); + } + return new DefaultUdtValue(this.type, buffers); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/data/IdentifierIndex.java b/core/src/main/java/com/datastax/oss/driver/internal/core/data/IdentifierIndex.java new file mode 100644 index 00000000000..0d649220df5 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/data/IdentifierIndex.java @@ -0,0 +1,72 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.data; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.data.AccessibleByName; +import com.datastax.oss.driver.api.core.data.GettableById; +import com.datastax.oss.driver.api.core.data.GettableByName; +import com.datastax.oss.driver.internal.core.util.Strings; +import com.datastax.oss.driver.shaded.guava.common.collect.Maps; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.Immutable; + +/** + * Indexes an ordered list of identifiers. + * + * @see GettableByName + * @see GettableById + */ +@Immutable +public class IdentifierIndex { + + private final Map byId; + private final Map byCaseSensitiveName; + private final Map byCaseInsensitiveName; + + public IdentifierIndex(List ids) { + this.byId = Maps.newHashMapWithExpectedSize(ids.size()); + this.byCaseSensitiveName = Maps.newHashMapWithExpectedSize(ids.size()); + this.byCaseInsensitiveName = Maps.newHashMapWithExpectedSize(ids.size()); + + int i = 0; + for (CqlIdentifier id : ids) { + byId.putIfAbsent(id, i); + byCaseSensitiveName.putIfAbsent(id.asInternal(), i); + byCaseInsensitiveName.putIfAbsent(id.asInternal().toLowerCase(), i); + i += 1; + } + } + + /** + * Returns the first occurrence of a given name, given the matching rules described in {@link + * AccessibleByName}, or -1 if it's not in the list. + */ + public int firstIndexOf(String name) { + Integer index = + (Strings.isDoubleQuoted(name)) + ? byCaseSensitiveName.get(Strings.unDoubleQuote(name)) + : byCaseInsensitiveName.get(name.toLowerCase()); + return (index == null) ? -1 : index; + } + + /** Returns the first occurrence of a given identifier, or -1 if it's not in the list. */ + public int firstIndexOf(CqlIdentifier id) { + Integer index = byId.get(id); + return (index == null) ? -1 : index; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/data/ValuesHelper.java b/core/src/main/java/com/datastax/oss/driver/internal/core/data/ValuesHelper.java new file mode 100644 index 00000000000..e33068621d0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/data/ValuesHelper.java @@ -0,0 +1,122 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.data; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.metadata.token.ByteOrderedToken; +import com.datastax.oss.driver.internal.core.metadata.token.Murmur3Token; +import com.datastax.oss.driver.internal.core.metadata.token.RandomToken; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import java.nio.ByteBuffer; +import java.util.List; + +public class ValuesHelper { + + public static ByteBuffer[] encodeValues( + Object[] values, + List fieldTypes, + CodecRegistry codecRegistry, + ProtocolVersion protocolVersion) { + Preconditions.checkArgument( + values.length <= fieldTypes.size(), + "Too many values (expected %s, got %s)", + fieldTypes.size(), + values.length); + + ByteBuffer[] encodedValues = new ByteBuffer[fieldTypes.size()]; + for (int i = 0; i < values.length; i++) { + Object value = values[i]; + ByteBuffer encodedValue; + if (value instanceof Token) { + if (value instanceof Murmur3Token) { + encodedValue = + TypeCodecs.BIGINT.encode(((Murmur3Token) value).getValue(), protocolVersion); + } else if (value instanceof ByteOrderedToken) { + encodedValue = + TypeCodecs.BLOB.encode(((ByteOrderedToken) value).getValue(), protocolVersion); + } else if (value instanceof RandomToken) { + encodedValue = + TypeCodecs.VARINT.encode(((RandomToken) value).getValue(), protocolVersion); + } else { + throw new IllegalArgumentException("Unsupported token type " + value.getClass()); + } + } else { + TypeCodec codec = + (value == null) + ? codecRegistry.codecFor(fieldTypes.get(i)) + : codecRegistry.codecFor(fieldTypes.get(i), value); + encodedValue = codec.encode(value, protocolVersion); + } + encodedValues[i] = encodedValue; + } + return encodedValues; + } + + public static ByteBuffer[] encodePreparedValues( + Object[] values, + ColumnDefinitions variableDefinitions, + CodecRegistry codecRegistry, + ProtocolVersion protocolVersion) { + + // Almost same as encodeValues, but we can't reuse because of variableDefinitions. Rebuilding a + // list of datatypes is not worth it, so duplicate the code. + + Preconditions.checkArgument( + values.length <= variableDefinitions.size(), + "Too many variables (expected %s, got %s)", + variableDefinitions.size(), + values.length); + + ByteBuffer[] encodedValues = new ByteBuffer[variableDefinitions.size()]; + int i; + for (i = 0; i < values.length; i++) { + Object value = values[i]; + ByteBuffer encodedValue; + if (value instanceof Token) { + if (value instanceof Murmur3Token) { + encodedValue = + TypeCodecs.BIGINT.encode(((Murmur3Token) value).getValue(), protocolVersion); + } else if (value instanceof ByteOrderedToken) { + encodedValue = + TypeCodecs.BLOB.encode(((ByteOrderedToken) value).getValue(), protocolVersion); + } else if (value instanceof RandomToken) { + encodedValue = + TypeCodecs.VARINT.encode(((RandomToken) value).getValue(), protocolVersion); + } else { + throw new IllegalArgumentException("Unsupported token type " + value.getClass()); + } + } else { + TypeCodec codec = + (value == null) + ? codecRegistry.codecFor(variableDefinitions.get(i).getType()) + : codecRegistry.codecFor(variableDefinitions.get(i).getType(), value); + encodedValue = codec.encode(value, protocolVersion); + } + encodedValues[i] = encodedValue; + } + for (; i < encodedValues.length; i++) { + encodedValues[i] = ProtocolConstants.UNSET_VALUE; + } + return encodedValues; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicy.java new file mode 100644 index 00000000000..31fafe8e228 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicy.java @@ -0,0 +1,325 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.loadbalancing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.metadata.TokenMap; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.metadata.MetadataManager; +import com.datastax.oss.driver.internal.core.util.ArrayUtils; +import com.datastax.oss.driver.internal.core.util.Reflection; +import com.datastax.oss.driver.internal.core.util.collection.QueryPlan; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntUnaryOperator; +import java.util.function.Predicate; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The default load balancing policy implementation. + * + *

To activate this policy, modify the {@code basic.load-balancing-policy} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   basic.load-balancing-policy {
+ *     class = DefaultLoadBalancingPolicy
+ *     local-datacenter = datacenter1
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class DefaultLoadBalancingPolicy implements LoadBalancingPolicy { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultLoadBalancingPolicy.class); + private static final Predicate INCLUDE_ALL_NODES = n -> true; + private static final IntUnaryOperator INCREMENT = i -> (i == Integer.MAX_VALUE) ? 0 : i + 1; + + private final String logPrefix; + private final MetadataManager metadataManager; + private final Predicate filter; + private final AtomicInteger roundRobinAmount = new AtomicInteger(); + private final boolean isDefaultPolicy; + @VisibleForTesting final CopyOnWriteArraySet localDcLiveNodes = new CopyOnWriteArraySet<>(); + + private volatile DistanceReporter distanceReporter; + @VisibleForTesting volatile String localDc; + + public DefaultLoadBalancingPolicy(@NonNull DriverContext context, @NonNull String profileName) { + InternalDriverContext internalContext = (InternalDriverContext) context; + + this.logPrefix = context.getSessionName() + "|" + profileName; + DriverExecutionProfile config = context.getConfig().getProfile(profileName); + this.localDc = getLocalDcFromConfig(internalContext, profileName, config); + this.isDefaultPolicy = profileName.equals(DriverExecutionProfile.DEFAULT_NAME); + + this.metadataManager = internalContext.getMetadataManager(); + + Predicate filterFromConfig = getFilterFromConfig(internalContext, profileName); + this.filter = + node -> { + String localDc1 = this.localDc; + if (localDc1 != null && !localDc1.equals(node.getDatacenter())) { + LOG.debug( + "[{}] Ignoring {} because it doesn't belong to the local DC {}", + logPrefix, + node, + localDc1); + return false; + } else if (!filterFromConfig.test(node)) { + LOG.debug( + "[{}] Ignoring {} because it doesn't match the user-provided predicate", + logPrefix, + node); + return false; + } else { + return true; + } + }; + } + + @Override + public void init(@NonNull Map nodes, @NonNull DistanceReporter distanceReporter) { + this.distanceReporter = distanceReporter; + + Set contactPoints = metadataManager.getContactPoints(); + if (localDc == null) { + if (metadataManager.wasImplicitContactPoint()) { + // We allow automatic inference of the local DC in this case + assert contactPoints.size() == 1; + Node contactPoint = contactPoints.iterator().next(); + localDc = contactPoint.getDatacenter(); + LOG.debug("[{}] Local DC set from contact point {}: {}", logPrefix, contactPoint, localDc); + } else { + throw new IllegalStateException( + "You provided explicit contact points, the local DC must be specified (see " + + DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER.getPath() + + " in the config)"); + } + } else { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Node node : contactPoints) { + String datacenter = node.getDatacenter(); + if (!Objects.equals(localDc, datacenter)) { + builder.put(node, (datacenter == null) ? "" : datacenter); + } + } + ImmutableMap badContactPoints = builder.build(); + if (isDefaultPolicy && !badContactPoints.isEmpty()) { + LOG.warn( + "[{}] You specified {} as the local DC, but some contact points are from a different DC ({})", + logPrefix, + localDc, + badContactPoints); + } + } + + for (Node node : nodes.values()) { + if (filter.test(node)) { + distanceReporter.setDistance(node, NodeDistance.LOCAL); + if (node.getState() != NodeState.DOWN) { + // This includes state == UNKNOWN. If the node turns out to be unreachable, this will be + // detected when we try to open a pool to it, it will get marked down and this will be + // signaled back to this policy + localDcLiveNodes.add(node); + } + } else { + distanceReporter.setDistance(node, NodeDistance.IGNORED); + } + } + } + + @NonNull + @Override + public Queue newQueryPlan(@Nullable Request request, @Nullable Session session) { + // Take a snapshot since the set is concurrent: + Object[] currentNodes = localDcLiveNodes.toArray(); + + Set allReplicas = getReplicas(request, session); + int replicaCount = 0; // in currentNodes + + if (!allReplicas.isEmpty()) { + // Move replicas to the beginning + for (int i = 0; i < currentNodes.length; i++) { + Node node = (Node) currentNodes[i]; + if (allReplicas.contains(node)) { + ArrayUtils.bubbleUp(currentNodes, i, replicaCount); + replicaCount += 1; + } + } + + if (replicaCount > 1) { + shuffleHead(currentNodes, replicaCount); + } + } + + LOG.trace("[{}] Prioritizing {} local replicas", logPrefix, replicaCount); + + // Round-robin the remaining nodes + ArrayUtils.rotate( + currentNodes, + replicaCount, + currentNodes.length - replicaCount, + roundRobinAmount.getAndUpdate(INCREMENT)); + + return new QueryPlan(currentNodes); + } + + private Set getReplicas(Request request, Session session) { + if (request == null || session == null) { + return Collections.emptySet(); + } + + // Note: we're on the hot path and the getXxx methods are potentially more than simple getters, + // so we only call each method when strictly necessary (which is why the code below looks a bit + // weird). + CqlIdentifier keyspace = request.getKeyspace(); + if (keyspace == null) { + keyspace = request.getRoutingKeyspace(); + } + if (keyspace == null && session.getKeyspace().isPresent()) { + keyspace = session.getKeyspace().get(); + } + if (keyspace == null) { + return Collections.emptySet(); + } + + Token token = request.getRoutingToken(); + ByteBuffer key = (token == null) ? request.getRoutingKey() : null; + if (token == null && key == null) { + return Collections.emptySet(); + } + + Optional maybeTokenMap = metadataManager.getMetadata().getTokenMap(); + if (maybeTokenMap.isPresent()) { + TokenMap tokenMap = maybeTokenMap.get(); + return (token != null) + ? tokenMap.getReplicas(keyspace, token) + : tokenMap.getReplicas(keyspace, key); + } else { + return Collections.emptySet(); + } + } + + @VisibleForTesting + protected void shuffleHead(Object[] currentNodes, int replicaCount) { + ArrayUtils.shuffleHead(currentNodes, replicaCount); + } + + @Override + public void onAdd(@NonNull Node node) { + if (filter.test(node)) { + LOG.debug("[{}] {} was added, setting distance to LOCAL", logPrefix, node); + // Setting to a non-ignored distance triggers the session to open a pool, which will in turn + // set the node UP when the first channel gets opened. + distanceReporter.setDistance(node, NodeDistance.LOCAL); + } else { + distanceReporter.setDistance(node, NodeDistance.IGNORED); + } + } + + @Override + public void onUp(@NonNull Node node) { + if (filter.test(node)) { + // Normally this is already the case, but the filter could be dynamic and have ignored the + // node previously. + distanceReporter.setDistance(node, NodeDistance.LOCAL); + if (localDcLiveNodes.add(node)) { + LOG.debug("[{}] {} came back UP, added to live set", logPrefix, node); + } + } else { + distanceReporter.setDistance(node, NodeDistance.IGNORED); + } + } + + @Override + public void onDown(@NonNull Node node) { + if (localDcLiveNodes.remove(node)) { + LOG.debug("[{}] {} went DOWN, removed from live set", logPrefix, node); + } + } + + @Override + public void onRemove(@NonNull Node node) { + if (localDcLiveNodes.remove(node)) { + LOG.debug("[{}] {} was removed, removed from live set", logPrefix, node); + } + } + + @Override + public void close() { + // nothing to do + } + + private String getLocalDcFromConfig( + InternalDriverContext internalContext, + @NonNull String profileName, + DriverExecutionProfile config) { + String localDc = internalContext.getLocalDatacenter(profileName); + if (localDc != null) { + LOG.debug("[{}] Local DC set from builder: {}", logPrefix, localDc); + } else { + localDc = config.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, null); + if (localDc != null) { + LOG.debug("[{}] Local DC set from configuration: {}", logPrefix, localDc); + } + } + return localDc; + } + + @SuppressWarnings("unchecked") + private Predicate getFilterFromConfig(InternalDriverContext context, String profileName) { + Predicate filterFromBuilder = context.getNodeFilter(profileName); + return (filterFromBuilder != null) + ? filterFromBuilder + : (Predicate) + Reflection.buildFromConfig( + context, + profileName, + DefaultDriverOption.LOAD_BALANCING_FILTER_CLASS, + Predicate.class) + .orElse(INCLUDE_ALL_NODES); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/AddNodeRefresh.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/AddNodeRefresh.java new file mode 100644 index 00000000000..088d5d0ea68 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/AddNodeRefresh.java @@ -0,0 +1,55 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.Map; +import java.util.UUID; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class AddNodeRefresh extends NodesRefresh { + + @VisibleForTesting final NodeInfo newNodeInfo; + + AddNodeRefresh(NodeInfo newNodeInfo) { + this.newNodeInfo = newNodeInfo; + } + + @Override + public Result compute( + DefaultMetadata oldMetadata, boolean tokenMapEnabled, InternalDriverContext context) { + Map oldNodes = oldMetadata.getNodes(); + if (oldNodes.containsKey(newNodeInfo.getHostId())) { + return new Result(oldMetadata); + } else { + DefaultNode newNode = new DefaultNode(newNodeInfo.getEndPoint(), context); + copyInfos(newNodeInfo, newNode, null, context.getSessionName()); + Map newNodes = + ImmutableMap.builder() + .putAll(oldNodes) + .put(newNode.getHostId(), newNode) + .build(); + return new Result( + oldMetadata.withNodes(newNodes, tokenMapEnabled, false, null, context), + ImmutableList.of(NodeStateEvent.added(newNode))); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultEndPoint.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultEndPoint.java new file mode 100644 index 00000000000..754497798f9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultEndPoint.java @@ -0,0 +1,84 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +public class DefaultEndPoint implements EndPoint { + + private final InetSocketAddress address; + private final String metricPrefix; + + public DefaultEndPoint(InetSocketAddress address) { + Preconditions.checkNotNull(address); + this.address = address; + this.metricPrefix = buildMetricPrefix(address); + } + + @Override + public InetSocketAddress resolve() { + return address; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof DefaultEndPoint) { + DefaultEndPoint that = (DefaultEndPoint) other; + return this.address.equals(that.address); + } else { + return false; + } + } + + @Override + public int hashCode() { + return address.hashCode(); + } + + @Override + public String toString() { + return address.toString(); + } + + @Override + public String asMetricPrefix() { + return metricPrefix; + } + + private static String buildMetricPrefix(InetSocketAddress addressAndPort) { + StringBuilder prefix = new StringBuilder(); + InetAddress address = addressAndPort.getAddress(); + int port = addressAndPort.getPort(); + if (address instanceof Inet4Address) { + // Metrics use '.' as a delimiter, replace so that the IP is a single path component + // (127.0.0.1 => 127_0_0_1) + prefix.append(address.getHostAddress().replace('.', '_')); + } else { + assert address instanceof Inet6Address; + // IPv6 only uses '%' and ':' as separators, so no replacement needed + prefix.append(address.getHostAddress()); + } + // Append the port since Cassandra 4 supports nodes with different ports + return prefix.append(':').append(port).toString(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultMetadata.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultMetadata.java new file mode 100644 index 00000000000..b8c4008775a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultMetadata.java @@ -0,0 +1,178 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.TokenMap; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.token.DefaultTokenMap; +import com.datastax.oss.driver.internal.core.metadata.token.ReplicationStrategyFactory; +import com.datastax.oss.driver.internal.core.metadata.token.TokenFactory; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.NanoTime; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import net.jcip.annotations.Immutable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class is immutable, so that metadata changes are atomic for the client. Every mutation + * operation must return a new instance, that will replace the existing one in {@link + * MetadataManager}'s volatile field. + */ +@Immutable +public class DefaultMetadata implements Metadata { + private static final Logger LOG = LoggerFactory.getLogger(DefaultMetadata.class); + public static DefaultMetadata EMPTY = + new DefaultMetadata(Collections.emptyMap(), Collections.emptyMap(), null); + + protected final Map nodes; + protected final Map keyspaces; + protected final TokenMap tokenMap; + + protected DefaultMetadata( + Map nodes, Map keyspaces, TokenMap tokenMap) { + this.nodes = nodes; + this.keyspaces = keyspaces; + this.tokenMap = tokenMap; + } + + @NonNull + @Override + public Map getNodes() { + return nodes; + } + + @NonNull + @Override + public Map getKeyspaces() { + return keyspaces; + } + + @NonNull + @Override + public Optional getTokenMap() { + return Optional.ofNullable(tokenMap); + } + + /** + * Refreshes the current metadata with the given list of nodes. + * + * @param tokenMapEnabled whether to rebuild the token map or not; if this is {@code false} the + * current token map will be copied into the new metadata without being recomputed. + * @param tokensChanged whether we observed a change of tokens for at least one node. This will + * require a full rebuild of the token map. + * @param tokenFactory only needed for the initial refresh, afterwards the existing one in the + * token map is used. + * @return the new metadata. + */ + public DefaultMetadata withNodes( + Map newNodes, + boolean tokenMapEnabled, + boolean tokensChanged, + TokenFactory tokenFactory, + InternalDriverContext context) { + + // Force a rebuild if at least one node has different tokens, or there are new or removed nodes. + boolean forceFullRebuild = tokensChanged || !newNodes.equals(nodes); + + return new DefaultMetadata( + ImmutableMap.copyOf(newNodes), + this.keyspaces, + rebuildTokenMap( + newNodes, keyspaces, tokenMapEnabled, forceFullRebuild, tokenFactory, context)); + } + + public DefaultMetadata withSchema( + Map newKeyspaces, + boolean tokenMapEnabled, + InternalDriverContext context) { + return new DefaultMetadata( + this.nodes, + ImmutableMap.copyOf(newKeyspaces), + rebuildTokenMap(nodes, newKeyspaces, tokenMapEnabled, false, null, context)); + } + + @Nullable + protected TokenMap rebuildTokenMap( + Map newNodes, + Map newKeyspaces, + boolean tokenMapEnabled, + boolean forceFullRebuild, + TokenFactory tokenFactory, + InternalDriverContext context) { + + String logPrefix = context.getSessionName(); + ReplicationStrategyFactory replicationStrategyFactory = context.getReplicationStrategyFactory(); + + if (!tokenMapEnabled) { + LOG.debug("[{}] Token map is disabled, skipping", logPrefix); + return this.tokenMap; + } + long start = System.nanoTime(); + try { + DefaultTokenMap oldTokenMap = (DefaultTokenMap) this.tokenMap; + if (oldTokenMap == null) { + // Initial build, we need the token factory + if (tokenFactory == null) { + LOG.debug( + "[{}] Building initial token map but the token factory is missing, skipping", + logPrefix); + return null; + } else { + LOG.debug("[{}] Building initial token map", logPrefix); + return DefaultTokenMap.build( + newNodes.values(), + newKeyspaces.values(), + tokenFactory, + replicationStrategyFactory, + logPrefix); + } + } else if (forceFullRebuild) { + LOG.debug( + "[{}] Updating token map but some nodes/tokens have changed, full rebuild", logPrefix); + return DefaultTokenMap.build( + newNodes.values(), + newKeyspaces.values(), + oldTokenMap.getTokenFactory(), + replicationStrategyFactory, + logPrefix); + } else { + LOG.debug("[{}] Refreshing token map (only schema has changed)", logPrefix); + return oldTokenMap.refresh( + newNodes.values(), newKeyspaces.values(), replicationStrategyFactory); + } + } catch (Throwable t) { + Loggers.warnWithException( + LOG, + "[{}] Unexpected error while refreshing token map, keeping previous version", + logPrefix, + t); + return this.tokenMap; + } finally { + LOG.debug("[{}] Rebuilding token map took {}", logPrefix, NanoTime.formatTimeSince(start)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultNode.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultNode.java new file mode 100644 index 00000000000..c8e6abc466c --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultNode.java @@ -0,0 +1,195 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metrics.NodeMetricUpdater; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import net.jcip.annotations.ThreadSafe; + +/** + * Implementation note: all the mutable state in this class is read concurrently, but only mutated + * from {@link MetadataManager}'s admin thread. + */ +@ThreadSafe +public class DefaultNode implements Node { + + private final EndPoint endPoint; + private final NodeMetricUpdater metricUpdater; + + volatile InetSocketAddress broadcastRpcAddress; + volatile InetSocketAddress broadcastAddress; + volatile InetSocketAddress listenAddress; + volatile String datacenter; + volatile String rack; + volatile Version cassandraVersion; + // Keep a copy of the raw tokens, to detect if they have changed when we refresh the node + volatile Set rawTokens; + volatile Map extras; + volatile UUID hostId; + volatile UUID schemaVersion; + + // These 4 fields are read concurrently, but only mutated on NodeStateManager's admin thread + volatile NodeState state; + volatile int openConnections; + volatile int reconnections; + volatile long upSinceMillis; + + volatile NodeDistance distance; + + public DefaultNode(EndPoint endPoint, InternalDriverContext context) { + this.endPoint = endPoint; + this.state = NodeState.UNKNOWN; + this.distance = NodeDistance.IGNORED; + this.rawTokens = Collections.emptySet(); + this.extras = Collections.emptyMap(); + // We leak a reference to a partially constructed object (this), but in practice this won't be a + // problem because the node updater only needs the connect address to initialize. + this.metricUpdater = context.getMetricsFactory().newNodeUpdater(this); + this.upSinceMillis = -1; + } + + @NonNull + @Override + public EndPoint getEndPoint() { + return endPoint; + } + + @NonNull + @Override + public Optional getBroadcastRpcAddress() { + return Optional.ofNullable(broadcastRpcAddress); + } + + @NonNull + @Override + public Optional getBroadcastAddress() { + return Optional.ofNullable(broadcastAddress); + } + + @NonNull + @Override + public Optional getListenAddress() { + return Optional.ofNullable(listenAddress); + } + + @NonNull + @Override + public String getDatacenter() { + return datacenter; + } + + @NonNull + @Override + public String getRack() { + return rack; + } + + @NonNull + @Override + public Version getCassandraVersion() { + return cassandraVersion; + } + + @NonNull + @Override + public UUID getHostId() { + return hostId; + } + + @NonNull + @Override + public UUID getSchemaVersion() { + return schemaVersion; + } + + @NonNull + @Override + public Map getExtras() { + return extras; + } + + @NonNull + @Override + public NodeState getState() { + return state; + } + + @Override + public long getUpSinceMillis() { + return upSinceMillis; + } + + @Override + public int getOpenConnections() { + return openConnections; + } + + @Override + public boolean isReconnecting() { + return reconnections > 0; + } + + @NonNull + @Override + public NodeDistance getDistance() { + return distance; + } + + public NodeMetricUpdater getMetricUpdater() { + return metricUpdater; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof Node) { + Node that = (Node) other; + // hostId is the natural identifier, but unfortunately we don't know it for contact points + // until the driver has opened the first connection. + return this.endPoint.equals(that.getEndPoint()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return endPoint.hashCode(); + } + + @Override + public String toString() { + return endPoint.toString(); + } + + /** Note: deliberately not exposed by the public interface. */ + public Set getRawTokens() { + return rawTokens; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultNodeInfo.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultNodeInfo.java new file mode 100644 index 00000000000..2bc3b8dfa54 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultNodeInfo.java @@ -0,0 +1,207 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import net.jcip.annotations.Immutable; +import net.jcip.annotations.NotThreadSafe; + +@Immutable +public class DefaultNodeInfo implements NodeInfo { + public static Builder builder() { + return new Builder(); + } + + private final EndPoint endPoint; + private final InetSocketAddress broadcastRpcAddress; + private final InetSocketAddress broadcastAddress; + private final InetSocketAddress listenAddress; + private final String datacenter; + private final String rack; + private final String cassandraVersion; + private final String partitioner; + private final Set tokens; + private final Map extras; + private final UUID hostId; + private final UUID schemaVersion; + + private DefaultNodeInfo(Builder builder) { + this.endPoint = builder.endPoint; + this.broadcastRpcAddress = builder.broadcastRpcAddress; + this.broadcastAddress = builder.broadcastAddress; + this.listenAddress = builder.listenAddress; + this.datacenter = builder.datacenter; + this.rack = builder.rack; + this.cassandraVersion = builder.cassandraVersion; + this.partitioner = builder.partitioner; + this.tokens = (builder.tokens == null) ? Collections.emptySet() : builder.tokens; + this.hostId = builder.hostId; + this.schemaVersion = builder.schemaVersion; + this.extras = (builder.extras == null) ? Collections.emptyMap() : builder.extras; + } + + @Override + public EndPoint getEndPoint() { + return endPoint; + } + + @Override + public Optional getBroadcastRpcAddress() { + return Optional.ofNullable(broadcastRpcAddress); + } + + @Override + public Optional getBroadcastAddress() { + return Optional.ofNullable(broadcastAddress); + } + + @Override + public Optional getListenAddress() { + return Optional.ofNullable(listenAddress); + } + + @Override + public String getDatacenter() { + return datacenter; + } + + @Override + public String getRack() { + return rack; + } + + @Override + public String getCassandraVersion() { + return cassandraVersion; + } + + @Override + public String getPartitioner() { + return partitioner; + } + + @Override + public Set getTokens() { + return tokens; + } + + @Override + public Map getExtras() { + return extras; + } + + @Override + public UUID getHostId() { + return hostId; + } + + @Override + public UUID getSchemaVersion() { + return schemaVersion; + } + + @NotThreadSafe + public static class Builder { + private EndPoint endPoint; + private InetSocketAddress broadcastRpcAddress; + private InetSocketAddress broadcastAddress; + private InetSocketAddress listenAddress; + private String datacenter; + private String rack; + private String cassandraVersion; + private String partitioner; + private Set tokens; + private Map extras; + private UUID hostId; + private UUID schemaVersion; + + public Builder withEndPoint(EndPoint endPoint) { + this.endPoint = endPoint; + return this; + } + + public Builder withBroadcastRpcAddress(InetSocketAddress address) { + this.broadcastRpcAddress = address; + return this; + } + + public Builder withBroadcastAddress(InetSocketAddress address) { + this.broadcastAddress = address; + return this; + } + + public Builder withListenAddress(InetSocketAddress address) { + this.listenAddress = address; + return this; + } + + public Builder withDatacenter(String datacenter) { + this.datacenter = datacenter; + return this; + } + + public Builder withRack(String rack) { + this.rack = rack; + return this; + } + + public Builder withCassandraVersion(String cassandraVersion) { + this.cassandraVersion = cassandraVersion; + return this; + } + + public Builder withPartitioner(String partitioner) { + this.partitioner = partitioner; + return this; + } + + public Builder withTokens(Set tokens) { + this.tokens = tokens; + return this; + } + + public Builder withHostId(UUID hostId) { + this.hostId = hostId; + return this; + } + + public Builder withSchemaVersion(UUID schemaVersion) { + this.schemaVersion = schemaVersion; + return this; + } + + public Builder withExtra(String key, Object value) { + if (value != null) { + if (this.extras == null) { + this.extras = new HashMap<>(); + } + this.extras.put(key, value); + } + return this; + } + + public DefaultNodeInfo build() { + return new DefaultNodeInfo(this); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultTopologyMonitor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultTopologyMonitor.java new file mode 100644 index 00000000000..e658dc21642 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DefaultTopologyMonitor.java @@ -0,0 +1,372 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRequestHandler; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.adminrequest.UnexpectedResponseException; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.control.ControlConnection; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.Error; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The default topology monitor, based on {@link ControlConnection}. + * + *

Note that event processing is implemented directly in the control connection, not here. + */ +@ThreadSafe +public class DefaultTopologyMonitor implements TopologyMonitor { + private static final Logger LOG = LoggerFactory.getLogger(DefaultTopologyMonitor.class); + + // Assume topology queries never need paging + private static final int INFINITE_PAGE_SIZE = -1; + + private final String logPrefix; + private final InternalDriverContext context; + private final ControlConnection controlConnection; + private final AddressTranslator addressTranslator; + private final Duration timeout; + private final boolean reconnectOnInit; + private final CompletableFuture closeFuture; + + @VisibleForTesting volatile boolean isSchemaV2; + @VisibleForTesting volatile int port = -1; + + public DefaultTopologyMonitor(InternalDriverContext context) { + this.logPrefix = context.getSessionName(); + this.context = context; + this.controlConnection = context.getControlConnection(); + this.addressTranslator = context.getAddressTranslator(); + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + this.timeout = config.getDuration(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT); + this.reconnectOnInit = config.getBoolean(DefaultDriverOption.RECONNECT_ON_INIT); + this.closeFuture = new CompletableFuture<>(); + // Set this to true initially, after the first refreshNodes is called this will either stay true + // or be set to false; + this.isSchemaV2 = true; + } + + @Override + public CompletionStage init() { + if (closeFuture.isDone()) { + return CompletableFutures.failedFuture(new IllegalStateException("closed")); + } + return controlConnection.init(true, reconnectOnInit, true); + } + + @Override + public CompletionStage initFuture() { + return controlConnection.initFuture(); + } + + @Override + public CompletionStage> refreshNode(Node node) { + if (closeFuture.isDone()) { + return CompletableFutures.failedFuture(new IllegalStateException("closed")); + } + LOG.debug("[{}] Refreshing info for {}", logPrefix, node); + DriverChannel channel = controlConnection.channel(); + if (node.getEndPoint().equals(channel.getEndPoint())) { + // refreshNode is called for nodes that just came up. If the control node just came up, it + // means the control connection just reconnected, which means we did a full node refresh. So + // we don't need to process this call. + LOG.debug("[{}] Ignoring refresh of control node", logPrefix); + return CompletableFuture.completedFuture(Optional.empty()); + } else if (node.getBroadcastAddress().isPresent()) { + CompletionStage query; + if (isSchemaV2) { + query = + query( + channel, + "SELECT * FROM " + + retrievePeerTableName() + + " WHERE peer = :address and peer_port = :port", + ImmutableMap.of( + "address", + node.getBroadcastAddress().get().getAddress(), + "peer", + node.getBroadcastAddress().get().getPort())); + } else { + query = + query( + channel, + "SELECT * FROM " + retrievePeerTableName() + " WHERE peer = :address", + ImmutableMap.of("address", node.getBroadcastAddress().get().getAddress())); + } + return query.thenApply(this::firstRowAsNodeInfo); + } else { + return query(channel, "SELECT * FROM " + retrievePeerTableName()) + .thenApply(result -> this.findInPeers(result, node.getHostId())); + } + } + + @Override + public CompletionStage> getNewNodeInfo(InetSocketAddress broadcastRpcAddress) { + if (closeFuture.isDone()) { + return CompletableFutures.failedFuture(new IllegalStateException("closed")); + } + LOG.debug("[{}] Fetching info for new node {}", logPrefix, broadcastRpcAddress); + DriverChannel channel = controlConnection.channel(); + return query(channel, "SELECT * FROM " + retrievePeerTableName()) + .thenApply(result -> this.findInPeers(result, broadcastRpcAddress)); + } + + @Override + public CompletionStage> refreshNodeList() { + if (closeFuture.isDone()) { + return CompletableFutures.failedFuture(new IllegalStateException("closed")); + } + LOG.debug("[{}] Refreshing node list", logPrefix); + DriverChannel channel = controlConnection.channel(); + + // This cast always succeeds in production. The only way it could fail is in a test that uses a + // local channel, and we don't have such tests at the moment. + InetSocketAddress controlBroadcastRpcAddress = + (InetSocketAddress) channel.getEndPoint().resolve(); + + savePort(channel); + + CompletionStage localQuery = query(channel, "SELECT * FROM system.local"); + CompletionStage peersV2Query = query(channel, "SELECT * FROM system.peers_v2"); + CompletableFuture peersQuery = new CompletableFuture<>(); + + peersV2Query.whenComplete( + (r, t) -> { + if (t != null) { + // If system.peers_v2 does not exist, downgrade to system.peers + if (t instanceof UnexpectedResponseException + && ((UnexpectedResponseException) t).message instanceof Error) { + Error error = (Error) ((UnexpectedResponseException) t).message; + if (error.code == ProtocolConstants.ErrorCode.INVALID + // Also downgrade on server error with a specific error message (DSE 6.0.0 to + // 6.0.2 with search enabled) + || (error.code == ProtocolConstants.ErrorCode.SERVER_ERROR + && error.message.contains("Unknown keyspace/cf pair (system.peers_v2)"))) { + this.isSchemaV2 = false; // We should not attempt this query in the future. + CompletableFutures.completeFrom( + query(channel, "SELECT * FROM system.peers"), peersQuery); + return; + } + } + peersQuery.completeExceptionally(t); + } else { + peersQuery.complete(r); + } + }); + + return localQuery.thenCombine( + peersQuery, + (controlNodeResult, peersResult) -> { + List nodeInfos = new ArrayList<>(); + // Don't rely on system.local.rpc_address for the control row, because it mistakenly + // reports the normal RPC address instead of the broadcast one (CASSANDRA-11181). We + // already know the address since we've just used it to query. + nodeInfos.add( + nodeInfoBuilder(controlNodeResult.iterator().next(), controlBroadcastRpcAddress) + .build()); + for (AdminRow row : peersResult) { + nodeInfos.add(asNodeInfo(row)); + } + return nodeInfos; + }); + } + + @Override + public CompletionStage checkSchemaAgreement() { + if (closeFuture.isDone()) { + return CompletableFuture.completedFuture(true); + } + DriverChannel channel = controlConnection.channel(); + return new SchemaAgreementChecker(channel, context, port, logPrefix).run(); + } + + @NonNull + @Override + public CompletionStage closeFuture() { + return closeFuture; + } + + @NonNull + @Override + public CompletionStage closeAsync() { + closeFuture.complete(null); + return closeFuture; + } + + @NonNull + @Override + public CompletionStage forceCloseAsync() { + return closeAsync(); + } + + @VisibleForTesting + protected CompletionStage query( + DriverChannel channel, String queryString, Map parameters) { + return AdminRequestHandler.query( + channel, queryString, parameters, timeout, INFINITE_PAGE_SIZE, logPrefix) + .start(); + } + + private CompletionStage query(DriverChannel channel, String queryString) { + return query(channel, queryString, Collections.emptyMap()); + } + + private String retrievePeerTableName() { + if (isSchemaV2) { + return "system.peers_v2"; + } + return "system.peers"; + } + + private NodeInfo asNodeInfo(AdminRow row) { + return nodeInfoBuilder(row, getBroadcastRpcAddress(row)).build(); + } + + private Optional firstRowAsNodeInfo(AdminResult result) { + Iterator iterator = result.iterator(); + if (iterator.hasNext()) { + return Optional.of(asNodeInfo(iterator.next())); + } else { + return Optional.empty(); + } + } + + /** + * @param broadcastRpcAddress this is a parameter only because we already have it when we come + * from {@link #findInPeers(AdminResult, InetSocketAddress)}. Callers that don't already have + * it can use {@link #getBroadcastRpcAddress}. + */ + protected DefaultNodeInfo.Builder nodeInfoBuilder( + AdminRow row, InetSocketAddress broadcastRpcAddress) { + + // Deployments that use a custom EndPoint implementation will need their own TopologyMonitor. + // One simple approach is to extend this class and override this method. + EndPoint endPoint = + new DefaultEndPoint(context.getAddressTranslator().translate(broadcastRpcAddress)); + + DefaultNodeInfo.Builder builder = + DefaultNodeInfo.builder() + .withEndPoint(endPoint) + .withBroadcastRpcAddress(broadcastRpcAddress); + InetAddress broadcastAddress = row.getInetAddress("broadcast_address"); // in system.local + if (broadcastAddress == null) { + broadcastAddress = row.getInetAddress("peer"); // in system.peers + } + int broadcastPort = 0; + if (row.contains("peer_port")) { + broadcastPort = row.getInteger("peer_port"); + } + builder.withBroadcastAddress(new InetSocketAddress(broadcastAddress, broadcastPort)); + InetAddress listenAddress = row.getInetAddress("listen_address"); + int listen_port = 0; + if (row.contains("listen_port")) { + listen_port = row.getInteger("listen_port"); + } + builder.withListenAddress(new InetSocketAddress(listenAddress, listen_port)); + builder.withDatacenter(row.getString("data_center")); + builder.withRack(row.getString("rack")); + builder.withCassandraVersion(row.getString("release_version")); + builder.withTokens(row.getSetOfString("tokens")); + builder.withPartitioner(row.getString("partitioner")); + builder.withHostId(row.getUuid("host_id")); + builder.withSchemaVersion(row.getUuid("schema_version")); + return builder; + } + + private Optional findInPeers( + AdminResult result, InetSocketAddress broadcastRpcAddressToFind) { + // The peers table is keyed by broadcast_address, but we only have the broadcast_rpc_address, so + // we have to traverse the whole table and check the rows one by one. + for (AdminRow row : result) { + InetSocketAddress broadcastRpcAddress = getBroadcastRpcAddress(row); + if (broadcastRpcAddress != null && broadcastRpcAddress.equals(broadcastRpcAddressToFind)) { + return Optional.of(nodeInfoBuilder(row, broadcastRpcAddress).build()); + } + } + LOG.debug("[{}] Could not find any peer row matching {}", logPrefix, broadcastRpcAddressToFind); + return Optional.empty(); + } + + private Optional findInPeers(AdminResult result, UUID hostIdToFind) { + for (AdminRow row : result) { + UUID hostId = row.getUuid("host_id"); + if (hostId != null && hostId.equals(hostIdToFind)) { + return Optional.of(nodeInfoBuilder(row, getBroadcastRpcAddress(row)).build()); + } + } + LOG.debug("[{}] Could not find any peer row matching {}", logPrefix, hostIdToFind); + return Optional.empty(); + } + + // Current versions of Cassandra (3.11 at the time of writing), require the same port for all + // nodes. As a consequence, the port is not stored in system tables. + // We save it the first time we get a control connection channel. + private void savePort(DriverChannel channel) { + if (port < 0) { + SocketAddress address = channel.getEndPoint().resolve(); + if (address instanceof InetSocketAddress) { + port = ((InetSocketAddress) address).getPort(); + } + } + } + + private InetSocketAddress getBroadcastRpcAddress(AdminRow row) { + InetAddress nativeAddress = row.getInetAddress("native_address"); + if (nativeAddress == null) { + // Cassandra < 4 + InetAddress rpcAddress = row.getInetAddress("rpc_address"); + if (rpcAddress == null) { + // This could only happen if system.peers is corrupted, but handle gracefully + return null; + } + return new InetSocketAddress(rpcAddress, port); + } else { + Integer rowPort = row.getInteger("native_port"); + if (rowPort == null || rowPort == 0) { + rowPort = port; + } + return new InetSocketAddress(nativeAddress, rowPort); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DistanceEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DistanceEvent.java new file mode 100644 index 00000000000..638d4f3db99 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/DistanceEvent.java @@ -0,0 +1,58 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +/** + * Indicates that the load balancing policy has assigned a new distance to a host. + * + *

This is informational only: firing this event manually does not change the distance. + */ +@Immutable +public class DistanceEvent { + public final NodeDistance distance; + public final DefaultNode node; + + public DistanceEvent(NodeDistance distance, DefaultNode node) { + this.distance = distance; + this.node = node; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof DistanceEvent) { + DistanceEvent that = (DistanceEvent) other; + return this.distance == that.distance && Objects.equals(this.node, that.node); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(this.distance, this.node); + } + + @Override + public String toString() { + return "DistanceEvent(" + distance + ", " + node + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/FullNodeListRefresh.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/FullNodeListRefresh.java new file mode 100644 index 00000000000..7b6aeae48e2 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/FullNodeListRefresh.java @@ -0,0 +1,116 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.token.DefaultTokenMap; +import com.datastax.oss.driver.internal.core.metadata.token.TokenFactory; +import com.datastax.oss.driver.internal.core.metadata.token.TokenFactoryRegistry; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.Sets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +class FullNodeListRefresh extends NodesRefresh { + + private static final Logger LOG = LoggerFactory.getLogger(FullNodeListRefresh.class); + + @VisibleForTesting final Iterable nodeInfos; + + FullNodeListRefresh(Iterable nodeInfos) { + this.nodeInfos = nodeInfos; + } + + @Override + public Result compute( + DefaultMetadata oldMetadata, boolean tokenMapEnabled, InternalDriverContext context) { + + String logPrefix = context.getSessionName(); + TokenFactoryRegistry tokenFactoryRegistry = context.getTokenFactoryRegistry(); + + Map oldNodes = oldMetadata.getNodes(); + + Map added = new HashMap<>(); + Set seen = new HashSet<>(); + + TokenFactory tokenFactory = + oldMetadata.getTokenMap().map(m -> ((DefaultTokenMap) m).getTokenFactory()).orElse(null); + boolean tokensChanged = false; + + for (NodeInfo nodeInfo : nodeInfos) { + UUID id = nodeInfo.getHostId(); + seen.add(id); + DefaultNode node = (DefaultNode) oldNodes.get(id); + if (node == null) { + node = new DefaultNode(nodeInfo.getEndPoint(), context); + LOG.debug("[{}] Adding new node {}", logPrefix, node); + added.put(id, node); + } + if (tokenFactory == null && nodeInfo.getPartitioner() != null) { + tokenFactory = tokenFactoryRegistry.tokenFactoryFor(nodeInfo.getPartitioner()); + } + tokensChanged |= copyInfos(nodeInfo, node, tokenFactory, logPrefix); + } + + Set removed = Sets.difference(oldNodes.keySet(), seen); + + if (added.isEmpty() && removed.isEmpty()) { // The list didn't change + if (!oldMetadata.getTokenMap().isPresent() && tokenFactory != null) { + // First time we found out what the partitioner is => set the token factory and trigger a + // token map rebuild: + return new Result( + oldMetadata.withNodes( + oldMetadata.getNodes(), tokenMapEnabled, true, tokenFactory, context)); + } else { + // No need to create a new metadata instance + return new Result(oldMetadata); + } + } else { + ImmutableMap.Builder newNodesBuilder = ImmutableMap.builder(); + ImmutableList.Builder eventsBuilder = ImmutableList.builder(); + + newNodesBuilder.putAll(added); + for (Map.Entry entry : oldNodes.entrySet()) { + if (!removed.contains(entry.getKey())) { + newNodesBuilder.put(entry.getKey(), entry.getValue()); + } + } + + for (Node node : added.values()) { + eventsBuilder.add(NodeStateEvent.added((DefaultNode) node)); + } + for (UUID id : removed) { + Node node = oldNodes.get(id); + eventsBuilder.add(NodeStateEvent.removed((DefaultNode) node)); + } + + return new Result( + oldMetadata.withNodes( + newNodesBuilder.build(), tokenMapEnabled, tokensChanged, tokenFactory, context), + eventsBuilder.build()); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/InitialNodeListRefresh.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/InitialNodeListRefresh.java new file mode 100644 index 00000000000..96ed3b0d19e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/InitialNodeListRefresh.java @@ -0,0 +1,109 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.token.DefaultTokenMap; +import com.datastax.oss.driver.internal.core.metadata.token.TokenFactory; +import com.datastax.oss.driver.internal.core.metadata.token.TokenFactoryRegistry; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.Set; +import java.util.UUID; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The first node list refresh: contact points are not in the metadata yet, we need to copy them + * over. + */ +@ThreadSafe +class InitialNodeListRefresh extends NodesRefresh { + + private static final Logger LOG = LoggerFactory.getLogger(InitialNodeListRefresh.class); + + @VisibleForTesting final Iterable nodeInfos; + @VisibleForTesting final Set contactPoints; + + InitialNodeListRefresh(Iterable nodeInfos, Set contactPoints) { + this.nodeInfos = nodeInfos; + this.contactPoints = contactPoints; + } + + @Override + public Result compute( + DefaultMetadata oldMetadata, boolean tokenMapEnabled, InternalDriverContext context) { + + String logPrefix = context.getSessionName(); + TokenFactoryRegistry tokenFactoryRegistry = context.getTokenFactoryRegistry(); + + assert oldMetadata.getNodes().isEmpty(); + + TokenFactory tokenFactory = + oldMetadata.getTokenMap().map(m -> ((DefaultTokenMap) m).getTokenFactory()).orElse(null); + boolean tokensChanged = false; + + ImmutableMap.Builder newNodesBuilder = ImmutableMap.builder(); + + for (NodeInfo nodeInfo : nodeInfos) { + EndPoint endPoint = nodeInfo.getEndPoint(); + DefaultNode node = findIn(contactPoints, endPoint); + if (node == null) { + node = new DefaultNode(endPoint, context); + LOG.debug("[{}] Adding new node {}", logPrefix, node); + } else { + LOG.debug("[{}] Copying contact point {}", logPrefix, node); + } + if (tokenFactory == null && nodeInfo.getPartitioner() != null) { + tokenFactory = tokenFactoryRegistry.tokenFactoryFor(nodeInfo.getPartitioner()); + } + tokensChanged |= copyInfos(nodeInfo, node, tokenFactory, logPrefix); + newNodesBuilder.put(node.getHostId(), node); + } + + ImmutableMap newNodes = newNodesBuilder.build(); + ImmutableList.Builder eventsBuilder = ImmutableList.builder(); + + for (DefaultNode newNode : newNodes.values()) { + if (!contactPoints.contains(newNode)) { + eventsBuilder.add(NodeStateEvent.added(newNode)); + } + } + for (DefaultNode contactPoint : contactPoints) { + if (findIn(newNodes.values(), contactPoint.getEndPoint()) == null) { + eventsBuilder.add(NodeStateEvent.removed(contactPoint)); + } + } + + return new Result( + oldMetadata.withNodes( + ImmutableMap.copyOf(newNodes), tokenMapEnabled, tokensChanged, tokenFactory, context), + eventsBuilder.build()); + } + + private DefaultNode findIn(Iterable nodes, EndPoint endPoint) { + for (Node node : nodes) { + if (node.getEndPoint().equals(endPoint)) { + return (DefaultNode) node; + } + } + return null; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapper.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapper.java new file mode 100644 index 00000000000..bdf0c392e0c --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapper.java @@ -0,0 +1,271 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.util.concurrent.ReplayingEventFilter; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import net.jcip.annotations.GuardedBy; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps the user-provided LBPs for internal use. This serves multiple purposes: + * + *
    + *
  • help enforce the guarantee that init is called exactly once, and before any other method. + *
  • handle the early stages of initialization (before first actual connect), where the LBPs are + * not ready yet. + *
  • handle incoming node state events from the outside world and propagate them to the + * policies. + *
  • process distance decisions from the policies and propagate them to the outside world. + *
+ */ +@ThreadSafe +public class LoadBalancingPolicyWrapper implements AutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(LoadBalancingPolicyWrapper.class); + + private enum State { + BEFORE_INIT, + DURING_INIT, + RUNNING, + CLOSING + } + + private final InternalDriverContext context; + private final Set policies; + private final Map policiesPerProfile; + private final Map reporters; + + private final Lock distancesLock = new ReentrantLock(); + + // Remember which distance each policy reported for each node. We assume that distance events will + // be rare, so don't try to be too clever, a global lock should suffice. + @GuardedBy("distancesLock") + private final Map> distances; + + private final String logPrefix; + private final ReplayingEventFilter eventFilter = + new ReplayingEventFilter<>(this::processNodeStateEvent); + private AtomicReference stateRef = new AtomicReference<>(State.BEFORE_INIT); + + public LoadBalancingPolicyWrapper( + @NonNull InternalDriverContext context, + @NonNull Map policiesPerProfile) { + this.context = context; + + this.policiesPerProfile = policiesPerProfile; + ImmutableMap.Builder reportersBuilder = + ImmutableMap.builder(); + // ImmutableMap.values does not remove duplicates, do it now so that we won't invoke a policy + // more than once if it's associated with multiple profiles + for (LoadBalancingPolicy policy : ImmutableSet.copyOf(policiesPerProfile.values())) { + reportersBuilder.put(policy, new SinglePolicyDistanceReporter(policy)); + } + this.reporters = reportersBuilder.build(); + // Just an alias to make the rest of the code more readable + this.policies = reporters.keySet(); + + this.distances = new HashMap<>(); + + this.logPrefix = context.getSessionName(); + context.getEventBus().register(NodeStateEvent.class, this::onNodeStateEvent); + } + + public void init() { + if (stateRef.compareAndSet(State.BEFORE_INIT, State.DURING_INIT)) { + LOG.debug("[{}] Initializing policies", logPrefix); + // State events can happen concurrently with init, so we must record them and replay once the + // policy is initialized. + eventFilter.start(); + MetadataManager metadataManager = context.getMetadataManager(); + Metadata metadata = metadataManager.getMetadata(); + for (LoadBalancingPolicy policy : policies) { + policy.init(metadata.getNodes(), reporters.get(policy)); + } + if (stateRef.compareAndSet(State.DURING_INIT, State.RUNNING)) { + eventFilter.markReady(); + } else { // closed during init + assert stateRef.get() == State.CLOSING; + for (LoadBalancingPolicy policy : policies) { + policy.close(); + } + } + } + } + + /** + * Note: we could infer the profile name from the request again in this method, but since that's + * already done in request processors, pass the value directly. + * + * @see LoadBalancingPolicy#newQueryPlan(Request, Session) + */ + @NonNull + public Queue newQueryPlan( + @Nullable Request request, @NonNull String executionProfileName, @Nullable Session session) { + switch (stateRef.get()) { + case BEFORE_INIT: + case DURING_INIT: + // The contact points are not stored in the metadata yet: + List nodes = new ArrayList<>(context.getMetadataManager().getContactPoints()); + Collections.shuffle(nodes); + return new ConcurrentLinkedQueue<>(nodes); + case RUNNING: + LoadBalancingPolicy policy = policiesPerProfile.get(executionProfileName); + if (policy == null) { + policy = policiesPerProfile.get(DriverExecutionProfile.DEFAULT_NAME); + } + return policy.newQueryPlan(request, session); + default: + return new ConcurrentLinkedQueue<>(); + } + } + + @NonNull + public Queue newQueryPlan() { + return newQueryPlan(null, DriverExecutionProfile.DEFAULT_NAME, null); + } + + // when it comes in from the outside + private void onNodeStateEvent(NodeStateEvent event) { + eventFilter.accept(event); + } + + // once it has gone through the filter + private void processNodeStateEvent(NodeStateEvent event) { + switch (stateRef.get()) { + case BEFORE_INIT: + case DURING_INIT: + throw new AssertionError("Filter should not be marked ready until LBP init"); + case CLOSING: + return; // ignore + case RUNNING: + for (LoadBalancingPolicy policy : policies) { + if (event.newState == NodeState.UP) { + policy.onUp(event.node); + } else if (event.newState == NodeState.DOWN || event.newState == NodeState.FORCED_DOWN) { + policy.onDown(event.node); + } else if (event.newState == NodeState.UNKNOWN) { + policy.onAdd(event.node); + } else if (event.newState == null) { + policy.onRemove(event.node); + } else { + LOG.warn("[{}] Unsupported event: {}", logPrefix, event); + } + } + break; + } + } + + @Override + public void close() { + State old; + while (true) { + old = stateRef.get(); + if (old == State.CLOSING) { + return; // already closed + } else if (stateRef.compareAndSet(old, State.CLOSING)) { + break; + } + } + // If BEFORE_INIT, no need to close because they were never initialized + // If DURING_INIT, this will be handled in init() + if (old == State.RUNNING) { + for (LoadBalancingPolicy policy : policies) { + policy.close(); + } + } + } + + // An individual distance reporter for one of the policies. The results are aggregated across all + // policies, the smallest distance for each node is used. + private class SinglePolicyDistanceReporter implements LoadBalancingPolicy.DistanceReporter { + + private final LoadBalancingPolicy policy; + + private SinglePolicyDistanceReporter(LoadBalancingPolicy policy) { + this.policy = policy; + } + + @Override + public void setDistance(@NonNull Node node, @NonNull NodeDistance suggestedDistance) { + LOG.debug( + "[{}] {} suggested {} to {}, checking what other policies said", + logPrefix, + policy, + node, + suggestedDistance); + distancesLock.lock(); + try { + Map distancesForNode = + distances.computeIfAbsent(node, (n) -> new HashMap<>()); + distancesForNode.put(policy, suggestedDistance); + NodeDistance newDistance = aggregate(distancesForNode); + LOG.debug("[{}] Shortest distance across all policies is {}", logPrefix, newDistance); + + // There is a small race condition here (check-then-act on a volatile field). However this + // would only happen if external code changes the distance, which is unlikely (and + // dangerous). + // The driver internals only ever set the distance here, and we're protected by the lock. + NodeDistance oldDistance = node.getDistance(); + if (!oldDistance.equals(newDistance)) { + LOG.debug("[{}] {} was {}, changing to {}", logPrefix, node, oldDistance, newDistance); + DefaultNode defaultNode = (DefaultNode) node; + defaultNode.distance = newDistance; + context.getEventBus().fire(new DistanceEvent(newDistance, defaultNode)); + } else { + LOG.debug("[{}] {} was already {}, ignoring", logPrefix, node, oldDistance); + } + } finally { + distancesLock.unlock(); + } + } + + private NodeDistance aggregate(Map distances) { + NodeDistance minimum = NodeDistance.IGNORED; + for (NodeDistance candidate : distances.values()) { + if (candidate.compareTo(minimum) < 0) { + minimum = candidate; + } + } + return minimum; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/MetadataManager.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/MetadataManager.java new file mode 100644 index 00000000000..2c4dcfa7f3e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/MetadataManager.java @@ -0,0 +1,486 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.AsyncAutoCloseable; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.config.ConfigChangeEvent; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.control.ControlConnection; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.SchemaParserFactory; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaQueriesFactory; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; +import com.datastax.oss.driver.internal.core.metadata.schema.refresh.SchemaRefresh; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.NanoTime; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.internal.core.util.concurrent.Debouncer; +import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.util.concurrent.EventExecutor; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Holds the immutable instance of the {@link Metadata}, and handles requests to update it. */ +@ThreadSafe +public class MetadataManager implements AsyncAutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(MetadataManager.class); + + static final EndPoint DEFAULT_CONTACT_POINT = + new DefaultEndPoint(new InetSocketAddress("127.0.0.1", 9042)); + + private final InternalDriverContext context; + private final String logPrefix; + private final EventExecutor adminExecutor; + private final DriverExecutionProfile config; + private final SingleThreaded singleThreaded; + private final ControlConnection controlConnection; + + private volatile DefaultMetadata metadata; // only updated from adminExecutor + private volatile boolean schemaEnabledInConfig; + private volatile List refreshedKeyspaces; + private volatile Boolean schemaEnabledProgrammatically; + private volatile boolean tokenMapEnabled; + private volatile Set contactPoints; + private volatile boolean wasImplicitContactPoint; + + public MetadataManager(InternalDriverContext context) { + this(context, DefaultMetadata.EMPTY); + } + + protected MetadataManager(InternalDriverContext context, DefaultMetadata initialMetadata) { + this.context = context; + this.metadata = initialMetadata; + this.logPrefix = context.getSessionName(); + this.adminExecutor = context.getNettyOptions().adminEventExecutorGroup().next(); + this.config = context.getConfig().getDefaultProfile(); + this.singleThreaded = new SingleThreaded(context, config); + this.controlConnection = context.getControlConnection(); + this.schemaEnabledInConfig = config.getBoolean(DefaultDriverOption.METADATA_SCHEMA_ENABLED); + this.refreshedKeyspaces = + config.getStringList( + DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, Collections.emptyList()); + this.tokenMapEnabled = config.getBoolean(DefaultDriverOption.METADATA_TOKEN_MAP_ENABLED); + + context.getEventBus().register(ConfigChangeEvent.class, this::onConfigChanged); + } + + private void onConfigChanged(@SuppressWarnings("unused") ConfigChangeEvent event) { + boolean schemaEnabledBefore = isSchemaEnabled(); + boolean tokenMapEnabledBefore = tokenMapEnabled; + List keyspacesBefore = this.refreshedKeyspaces; + + this.schemaEnabledInConfig = config.getBoolean(DefaultDriverOption.METADATA_SCHEMA_ENABLED); + this.refreshedKeyspaces = + config.getStringList( + DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, Collections.emptyList()); + this.tokenMapEnabled = config.getBoolean(DefaultDriverOption.METADATA_TOKEN_MAP_ENABLED); + + if ((!schemaEnabledBefore + || !keyspacesBefore.equals(refreshedKeyspaces) + || (!tokenMapEnabledBefore && tokenMapEnabled)) + && isSchemaEnabled()) { + refreshSchema(null, false, true); + } + } + + public Metadata getMetadata() { + return this.metadata; + } + + public void addContactPoints(Set providedContactPoints) { + // Convert the EndPoints to Nodes, but we can't put them into the Metadata yet, because we + // don't know their host_id. So store them in a volatile field instead, they will get copied + // during the first node refresh. + ImmutableSet.Builder contactPointsBuilder = ImmutableSet.builder(); + if (providedContactPoints == null || providedContactPoints.isEmpty()) { + LOG.info( + "[{}] No contact points provided, defaulting to {}", logPrefix, DEFAULT_CONTACT_POINT); + this.wasImplicitContactPoint = true; + contactPointsBuilder.add(new DefaultNode(DEFAULT_CONTACT_POINT, context)); + } else { + for (EndPoint endPoint : providedContactPoints) { + contactPointsBuilder.add(new DefaultNode(endPoint, context)); + } + } + this.contactPoints = contactPointsBuilder.build(); + LOG.debug("[{}] Adding initial contact points {}", logPrefix, contactPoints); + } + + /** + * The contact points that were used by the driver to initialize. If none were provided + * explicitly, this will be the default (127.0.0.1:9042). + * + * @see #wasImplicitContactPoint() + */ + public Set getContactPoints() { + return contactPoints; + } + + /** Whether the default contact point was used (because none were provided explicitly). */ + public boolean wasImplicitContactPoint() { + return wasImplicitContactPoint; + } + + public CompletionStage refreshNodes() { + return context + .getTopologyMonitor() + .refreshNodeList() + .thenApplyAsync(singleThreaded::refreshNodes, adminExecutor); + } + + public CompletionStage refreshNode(Node node) { + return context + .getTopologyMonitor() + .refreshNode(node) + .thenApplyAsync( + maybeInfo -> { + if (maybeInfo.isPresent()) { + boolean tokensChanged = + NodesRefresh.copyInfos(maybeInfo.get(), (DefaultNode) node, null, logPrefix); + if (tokensChanged) { + apply(new TokensChangedRefresh()); + } + } else { + LOG.debug( + "[{}] Topology monitor did not return any info for the refresh of {}, skipping", + logPrefix, + node); + } + return null; + }, + adminExecutor); + } + + public void addNode(InetSocketAddress broadcastRpcAddress) { + context + .getTopologyMonitor() + .getNewNodeInfo(broadcastRpcAddress) + .whenCompleteAsync( + (info, error) -> { + if (error != null) { + LOG.debug( + "[{}] Error refreshing node info for {}, " + + "this will be retried on the next full refresh", + logPrefix, + broadcastRpcAddress, + error); + } else { + singleThreaded.addNode(broadcastRpcAddress, info.orElse(null)); + } + }, + adminExecutor); + } + + public void removeNode(InetSocketAddress broadcastRpcAddress) { + RunOrSchedule.on(adminExecutor, () -> singleThreaded.removeNode(broadcastRpcAddress)); + } + + /** + * @param keyspace if this refresh was triggered by an event, that event's keyspace, otherwise + * null (this is only used to discard the event if it targets a keyspace that we're ignoring) + * @param evenIfDisabled force the refresh even if schema is currently disabled (used for user + * request) + * @param flushNow bypass the debouncer and force an immediate refresh (used to avoid a delay at + * startup) + */ + public CompletionStage refreshSchema( + String keyspace, boolean evenIfDisabled, boolean flushNow) { + CompletableFuture future = new CompletableFuture<>(); + RunOrSchedule.on( + adminExecutor, + () -> singleThreaded.refreshSchema(keyspace, evenIfDisabled, flushNow, future)); + return future; + } + + public boolean isSchemaEnabled() { + return (schemaEnabledProgrammatically != null) + ? schemaEnabledProgrammatically + : schemaEnabledInConfig; + } + + public CompletionStage setSchemaEnabled(Boolean newValue) { + boolean wasEnabledBefore = isSchemaEnabled(); + schemaEnabledProgrammatically = newValue; + if (!wasEnabledBefore && isSchemaEnabled()) { + return refreshSchema(null, false, true); + } else { + return CompletableFuture.completedFuture(metadata); + } + } + + /** + * Returns a future that completes after the first schema refresh attempt, whether that attempt + * succeeded or not (we wait for that refresh at init, but if it fails it's not fatal). + */ + public CompletionStage firstSchemaRefreshFuture() { + return singleThreaded.firstSchemaRefreshFuture; + } + + @NonNull + @Override + public CompletionStage closeFuture() { + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage closeAsync() { + RunOrSchedule.on(adminExecutor, singleThreaded::close); + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage forceCloseAsync() { + return this.closeAsync(); + } + + private class SingleThreaded { + private final CompletableFuture closeFuture = new CompletableFuture<>(); + private boolean closeWasCalled; + private final CompletableFuture firstSchemaRefreshFuture = new CompletableFuture<>(); + private final Debouncer, CompletableFuture> + schemaRefreshDebouncer; + private final SchemaQueriesFactory schemaQueriesFactory; + private final SchemaParserFactory schemaParserFactory; + + // We don't allow concurrent schema refreshes. If one is already running, the next one is queued + // (and the ones after that are merged with the queued one). + private CompletableFuture currentSchemaRefresh; + private CompletableFuture queuedSchemaRefresh; + + private boolean didFirstNodeListRefresh; + + private SingleThreaded(InternalDriverContext context, DriverExecutionProfile config) { + this.schemaRefreshDebouncer = + new Debouncer<>( + adminExecutor, + this::coalesceSchemaRequests, + this::startSchemaRequest, + config.getDuration(DefaultDriverOption.METADATA_SCHEMA_WINDOW), + config.getInt(DefaultDriverOption.METADATA_SCHEMA_MAX_EVENTS)); + this.schemaQueriesFactory = context.getSchemaQueriesFactory(); + this.schemaParserFactory = context.getSchemaParserFactory(); + } + + private Void refreshNodes(Iterable nodeInfos) { + MetadataRefresh refresh = + didFirstNodeListRefresh + ? new FullNodeListRefresh(nodeInfos) + : new InitialNodeListRefresh(nodeInfos, contactPoints); + didFirstNodeListRefresh = true; + return apply(refresh); + } + + private void addNode(InetSocketAddress address, NodeInfo info) { + try { + if (info != null) { + if (!address.equals(info.getBroadcastRpcAddress().orElse(null))) { + // This would be a bug in the TopologyMonitor, protect against it + LOG.warn( + "[{}] Received a request to add a node for broadcast RPC address {}, " + + "but the provided info reports {}, ignoring it", + logPrefix, + address, + info.getBroadcastAddress()); + } else { + apply(new AddNodeRefresh(info)); + } + } else { + LOG.debug( + "[{}] Ignoring node addition for {} because the " + + "topology monitor didn't return any information", + logPrefix, + address); + } + } catch (Throwable t) { + LOG.warn("[" + logPrefix + "] Unexpected exception while handling added node", logPrefix); + } + } + + private void removeNode(InetSocketAddress broadcastRpcAddress) { + apply(new RemoveNodeRefresh(broadcastRpcAddress)); + } + + private void refreshSchema( + String keyspace, + boolean evenIfDisabled, + boolean flushNow, + CompletableFuture future) { + + if (!didFirstNodeListRefresh) { + // This happen if the control connection receives a schema event during init. We can't + // refresh yet because we don't know the nodes' versions, simply ignore. + future.complete(metadata); + return; + } + + // If this is an event, make sure it's not targeting a keyspace that we're ignoring. + boolean isRefreshedKeyspace = + keyspace == null || refreshedKeyspaces.isEmpty() || refreshedKeyspaces.contains(keyspace); + + if (isRefreshedKeyspace && (evenIfDisabled || isSchemaEnabled())) { + acceptSchemaRequest(future, flushNow); + } else { + future.complete(metadata); + singleThreaded.firstSchemaRefreshFuture.complete(null); + } + } + + // An external component has requested a schema refresh, feed it to the debouncer. + private void acceptSchemaRequest(CompletableFuture future, boolean flushNow) { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + future.complete(metadata); + } else { + schemaRefreshDebouncer.receive(future); + if (flushNow) { + schemaRefreshDebouncer.flushNow(); + } + } + } + + // Multiple requests have arrived within the debouncer window, coalesce them. + private CompletableFuture coalesceSchemaRequests( + List> futures) { + assert adminExecutor.inEventLoop(); + assert !futures.isEmpty(); + // Keep only one, but ensure that the discarded ones will still be completed when we're done + CompletableFuture result = null; + for (CompletableFuture future : futures) { + if (result == null) { + result = future; + } else { + CompletableFutures.completeFrom(result, future); + } + } + return result; + } + + // The debouncer has flushed, start the actual work. + private void startSchemaRequest(CompletableFuture future) { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + future.complete(metadata); + return; + } + if (currentSchemaRefresh == null) { + currentSchemaRefresh = future; + LOG.debug("[{}] Starting schema refresh", logPrefix); + maybeInitControlConnection() + .thenCompose(v -> context.getTopologyMonitor().checkSchemaAgreement()) + // 1. Query system tables + .thenCompose(b -> schemaQueriesFactory.newInstance(future).execute()) + // 2. Parse the rows into metadata objects, put them in a MetadataRefresh + // 3. Apply the MetadataRefresh + .thenApplyAsync(this::parseAndApplySchemaRows, adminExecutor) + .whenComplete( + (v, error) -> { + if (error != null) { + Loggers.warnWithException( + LOG, + "[{}] Unexpected error while refreshing schema, skipping", + logPrefix, + error); + } + singleThreaded.firstSchemaRefreshFuture.complete(null); + }); + } else if (queuedSchemaRefresh == null) { + queuedSchemaRefresh = future; // wait for our turn + } else { + CompletableFutures.completeFrom(queuedSchemaRefresh, future); // join the queued request + } + } + + // The control connection may or may not have been initialized already by TopologyMonitor. + private CompletionStage maybeInitControlConnection() { + if (firstSchemaRefreshFuture.isDone()) { + // Not the first schema refresh, so we know init was attempted already + return firstSchemaRefreshFuture; + } else { + controlConnection.init(false, true, false); + // The control connection might fail to connect and reattempt, but for the metadata refresh + // that led us here we only care about the first attempt (metadata is not vital, so if we + // can't get it right now it's OK to move on) + return controlConnection.firstConnectionAttemptFuture(); + } + } + + private Void parseAndApplySchemaRows(SchemaRows schemaRows) { + assert adminExecutor.inEventLoop(); + assert schemaRows.refreshFuture() == currentSchemaRefresh; + try { + SchemaRefresh schemaRefresh = schemaParserFactory.newInstance(schemaRows).parse(); + long start = System.nanoTime(); + apply(schemaRefresh); + currentSchemaRefresh.complete(metadata); + LOG.debug( + "[{}] Applying schema refresh took {}", logPrefix, NanoTime.formatTimeSince(start)); + } catch (Throwable t) { + currentSchemaRefresh.completeExceptionally(t); + } + currentSchemaRefresh = null; + if (queuedSchemaRefresh != null) { + CompletableFuture tmp = this.queuedSchemaRefresh; + this.queuedSchemaRefresh = null; + startSchemaRequest(tmp); + } + return null; + } + + private void close() { + if (closeWasCalled) { + return; + } + closeWasCalled = true; + LOG.debug("[{}] Closing", logPrefix); + // The current schema refresh should fail when its channel gets closed. + if (queuedSchemaRefresh != null) { + queuedSchemaRefresh.completeExceptionally(new IllegalStateException("Cluster is closed")); + } + closeFuture.complete(null); + } + } + + @VisibleForTesting + Void apply(MetadataRefresh refresh) { + assert adminExecutor.inEventLoop(); + MetadataRefresh.Result result = refresh.compute(metadata, tokenMapEnabled, context); + metadata = result.newMetadata; + boolean isFirstSchemaRefresh = + refresh instanceof SchemaRefresh && !singleThreaded.firstSchemaRefreshFuture.isDone(); + if (!singleThreaded.closeWasCalled && !isFirstSchemaRefresh) { + for (Object event : result.events) { + context.getEventBus().fire(event); + } + } + return null; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/MetadataRefresh.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/MetadataRefresh.java new file mode 100644 index 00000000000..aab77f2a756 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/MetadataRefresh.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import java.util.Collections; +import java.util.List; + +/** + * Any update to the driver's metadata. It produces a new metadata instance, and may also trigger + * events. + * + *

This is modelled as a separate type for modularity, and because we can't send the events while + * we are doing the refresh (by contract, the new copy of the metadata needs to be visible before + * the events are sent). This also makes unit testing very easy. + * + *

This is only instantiated and called from {@link MetadataManager}'s admin thread, therefore + * implementations don't need to be thread-safe. + * + * @see Session#getMetadata() + */ +public interface MetadataRefresh { + + Result compute( + DefaultMetadata oldMetadata, boolean tokenMapEnabled, InternalDriverContext context); + + class Result { + public final DefaultMetadata newMetadata; + public final List events; + + public Result(DefaultMetadata newMetadata, List events) { + this.newMetadata = newMetadata; + this.events = events; + } + + public Result(DefaultMetadata newMetadata) { + this(newMetadata, Collections.emptyList()); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodeInfo.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodeInfo.java new file mode 100644 index 00000000000..11aaf2c00ea --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodeInfo.java @@ -0,0 +1,126 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +/** + * Information about a node, returned by the {@link TopologyMonitor}. + * + *

This information will be copied to the corresponding {@link Node} in the metadata. + */ +public interface NodeInfo { + + /** The endpoint that the driver will use to connect to the node. */ + EndPoint getEndPoint(); + + /** + * The node's broadcast RPC address. + * + *

This is used to match status events coming in on the control connection. Note that it's not + * possible to fill it for the control node for some Cassandra versions, but that's less important + * because the control node doesn't receive events for itself. + * + * @see Node#getBroadcastRpcAddress() + */ + Optional getBroadcastRpcAddress(); + + /** + * The node's broadcast address and port. That is, the address that other nodes use to communicate + * with that node. + * + *

This is only used by the default topology monitor, so if you are writing a custom one and + * don't need this information, you can leave it empty. + */ + Optional getBroadcastAddress(); + + /** + * The node's listen address and port. That is, the address that the Cassandra process binds to. + * + *

This is currently not used anywhere in the driver. If you write a custom topology monitor + * and don't need this information, you can leave it empty. + */ + Optional getListenAddress(); + + /** + * The data center that this node belongs to, according to the Cassandra snitch. + * + *

This is used by some {@link LoadBalancingPolicy} implementations to compute the {@link + * NodeDistance}. + */ + String getDatacenter(); + + /** + * The rack that this node belongs to, according to the Cassandra snitch. + * + *

This is used by some {@link LoadBalancingPolicy} implementations to compute the {@link + * NodeDistance}. + */ + String getRack(); + + /** + * The Cassandra version that this node runs. + * + *

This is used when parsing the schema (schema tables sometimes change from one version to the + * next, even if the protocol version stays the same). If this is null, schema parsing will use + * the lowest version for the current protocol version, which might lead to inaccuracies. + */ + String getCassandraVersion(); + + /** + * The fully-qualifier name of the partitioner class that distributes data across the nodes, as it + * appears in {@code system.local.partitioner}. + * + *

This is used to compute the driver-side token metadata (in particular, token-aware routing + * relies on this information). It is only really needed for the first node of the initial node + * list refresh (but it doesn't hurt to always include it if possible). If it is absent, {@link + * Metadata#getTokenMap()} will remain empty. + */ + String getPartitioner(); + + /** + * The tokens that this node owns on the ring. + * + *

This is used to compute the driver-side token metadata (in particular, token-aware routing + * relies on this information). If you're not using token metadata in any way, you may return an + * empty set here. + */ + Set getTokens(); + + /** + * An additional map of free-form properties, that can be used by custom implementations. They + * will be copied as-is into {@link Node#getExtras()}. + */ + Map getExtras(); + + /** + * The host ID that is assigned to this host by cassandra. The driver uses this to uniquely + * identify a node. + */ + UUID getHostId(); + + /** The current version that is associated with the nodes schema. */ + UUID getSchemaVersion(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodeStateEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodeStateEvent.java new file mode 100644 index 00000000000..8a5d9e54f48 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodeStateEvent.java @@ -0,0 +1,72 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class NodeStateEvent { + public static NodeStateEvent changed(NodeState oldState, NodeState newState, DefaultNode node) { + Preconditions.checkNotNull(oldState); + Preconditions.checkNotNull(newState); + return new NodeStateEvent(oldState, newState, node); + } + + public static NodeStateEvent added(DefaultNode node) { + return new NodeStateEvent(null, NodeState.UNKNOWN, node); + } + + public static NodeStateEvent removed(DefaultNode node) { + return new NodeStateEvent(null, null, node); + } + + public final NodeState oldState; + public final NodeState newState; + public final DefaultNode node; + + private NodeStateEvent(NodeState oldState, NodeState newState, DefaultNode node) { + this.node = node; + this.oldState = oldState; + this.newState = newState; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof NodeStateEvent) { + NodeStateEvent that = (NodeStateEvent) other; + return this.oldState == that.oldState + && this.newState == that.newState + && Objects.equals(this.node, that.node); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(oldState, newState, node); + } + + @Override + public String toString() { + return "NodeStateEvent(" + oldState + "=>" + newState + ", " + node + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodeStateManager.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodeStateManager.java new file mode 100644 index 00000000000..b2f264f30f9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodeStateManager.java @@ -0,0 +1,349 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.AsyncAutoCloseable; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.internal.core.channel.ChannelEvent; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.concurrent.Debouncer; +import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule; +import com.datastax.oss.driver.shaded.guava.common.collect.Maps; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.util.concurrent.EventExecutor; +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains the state of the Cassandra nodes, based on the events received from other components of + * the driver. + * + *

See {@link NodeState} and {@link TopologyEvent} for a description of the state change rules. + */ +@ThreadSafe +public class NodeStateManager implements AsyncAutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(NodeStateManager.class); + + private final EventExecutor adminExecutor; + private final SingleThreaded singleThreaded; + private final String logPrefix; + + public NodeStateManager(InternalDriverContext context) { + this.adminExecutor = context.getNettyOptions().adminEventExecutorGroup().next(); + this.singleThreaded = new SingleThreaded(context); + this.logPrefix = context.getSessionName(); + } + + /** + * Indicates when the driver initialization is complete (that is, we have performed the first node + * list refresh and are about to initialize the load balancing policy). + */ + public void markInitialized() { + RunOrSchedule.on(adminExecutor, singleThreaded::markInitialized); + } + + @NonNull + @Override + public CompletionStage closeFuture() { + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage closeAsync() { + RunOrSchedule.on(adminExecutor, singleThreaded::close); + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage forceCloseAsync() { + return closeAsync(); + } + + private class SingleThreaded { + + private final MetadataManager metadataManager; + private final EventBus eventBus; + private final Debouncer> topologyEventDebouncer; + private final CompletableFuture closeFuture = new CompletableFuture<>(); + private boolean isInitialized = false; + private boolean closeWasCalled; + + private SingleThreaded(InternalDriverContext context) { + this.metadataManager = context.getMetadataManager(); + + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + this.topologyEventDebouncer = + new Debouncer<>( + adminExecutor, + this::coalesceTopologyEvents, + this::flushTopologyEvents, + config.getDuration(DefaultDriverOption.METADATA_TOPOLOGY_WINDOW), + config.getInt(DefaultDriverOption.METADATA_TOPOLOGY_MAX_EVENTS)); + + this.eventBus = context.getEventBus(); + this.eventBus.register( + ChannelEvent.class, RunOrSchedule.on(adminExecutor, this::onChannelEvent)); + this.eventBus.register( + TopologyEvent.class, RunOrSchedule.on(adminExecutor, this::onTopologyEvent)); + // Note: this component exists for the whole life of the driver instance, so don't worry about + // unregistering the listeners. + } + + private void markInitialized() { + assert adminExecutor.inEventLoop(); + isInitialized = true; + } + + // Updates to DefaultNode's volatile fields are confined to the admin thread + @SuppressWarnings("NonAtomicVolatileUpdate") + private void onChannelEvent(ChannelEvent event) { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + return; + } + LOG.debug("[{}] Processing {}", logPrefix, event); + DefaultNode node = (DefaultNode) event.node; + assert node != null; + switch (event.type) { + case OPENED: + node.openConnections += 1; + if (node.state == NodeState.DOWN || node.state == NodeState.UNKNOWN) { + setState(node, NodeState.UP, "a new connection was opened to it"); + } + break; + case CLOSED: + node.openConnections -= 1; + if (node.openConnections == 0 && node.reconnections > 0) { + setState(node, NodeState.DOWN, "it was reconnecting and lost its last connection"); + } + break; + case RECONNECTION_STARTED: + node.reconnections += 1; + if (node.openConnections == 0) { + setState(node, NodeState.DOWN, "it has no connections and started reconnecting"); + } + break; + case RECONNECTION_STOPPED: + node.reconnections -= 1; + break; + case CONTROL_CONNECTION_FAILED: + // Special case for init, where this means that a contact point is down. In other + // situations that information is not really useful, we rely on + // openConnections/reconnections instead. + if (!isInitialized) { + setState(node, NodeState.DOWN, "it was tried as a contact point but failed"); + } + break; + } + } + + private void onDebouncedTopologyEvent(TopologyEvent event) { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + return; + } + LOG.debug("[{}] Processing {}", logPrefix, event); + Optional maybeNode = metadataManager.getMetadata().findNode(event.broadcastRpcAddress); + switch (event.type) { + case SUGGEST_UP: + if (maybeNode.isPresent()) { + DefaultNode node = (DefaultNode) maybeNode.get(); + if (node.state == NodeState.FORCED_DOWN) { + LOG.debug("[{}] Not setting {} UP because it is FORCED_DOWN", logPrefix, node); + } else if (node.distance == NodeDistance.IGNORED) { + setState(node, NodeState.UP, "it is IGNORED and an UP topology event was received"); + } + } else { + LOG.debug( + "[{}] Received UP event for unknown node {}, adding it", + logPrefix, + event.broadcastRpcAddress); + metadataManager.addNode(event.broadcastRpcAddress); + } + break; + case SUGGEST_DOWN: + if (maybeNode.isPresent()) { + DefaultNode node = (DefaultNode) maybeNode.get(); + if (node.openConnections > 0) { + LOG.debug( + "[{}] Not setting {} DOWN because it still has active connections", + logPrefix, + node); + } else if (node.state == NodeState.FORCED_DOWN) { + LOG.debug("[{}] Not setting {} DOWN because it is FORCED_DOWN", logPrefix, node); + } else if (node.distance == NodeDistance.IGNORED) { + setState( + node, NodeState.DOWN, "it is IGNORED and a DOWN topology event was received"); + } + } else { + LOG.debug( + "[{}] Received DOWN event for unknown node {}, ignoring it", + logPrefix, + event.broadcastRpcAddress); + } + break; + case FORCE_UP: + if (maybeNode.isPresent()) { + DefaultNode node = (DefaultNode) maybeNode.get(); + setState(node, NodeState.UP, "a FORCE_UP topology event was received"); + } else { + LOG.debug( + "[{}] Received FORCE_UP event for unknown node {}, adding it", + logPrefix, + event.broadcastRpcAddress); + metadataManager.addNode(event.broadcastRpcAddress); + } + break; + case FORCE_DOWN: + if (maybeNode.isPresent()) { + DefaultNode node = (DefaultNode) maybeNode.get(); + setState(node, NodeState.FORCED_DOWN, "a FORCE_DOWN topology event was received"); + } else { + LOG.debug( + "[{}] Received FORCE_DOWN event for unknown node {}, ignoring it", + logPrefix, + event.broadcastRpcAddress); + } + break; + case SUGGEST_ADDED: + if (maybeNode.isPresent()) { + DefaultNode node = (DefaultNode) maybeNode.get(); + LOG.debug( + "[{}] Received ADDED event for {} but it is already in our metadata, ignoring", + logPrefix, + node); + } else { + metadataManager.addNode(event.broadcastRpcAddress); + } + break; + case SUGGEST_REMOVED: + if (maybeNode.isPresent()) { + metadataManager.removeNode(event.broadcastRpcAddress); + } else { + LOG.debug( + "[{}] Received REMOVED event for {} but it is not in our metadata, ignoring", + logPrefix, + event.broadcastRpcAddress); + } + break; + } + } + + // Called by the event bus, needs debouncing + private void onTopologyEvent(TopologyEvent event) { + assert adminExecutor.inEventLoop(); + topologyEventDebouncer.receive(event); + } + + // Called to process debounced events before flushing + private Collection coalesceTopologyEvents(List events) { + assert adminExecutor.inEventLoop(); + Collection result; + if (events.size() == 1) { + result = events; + } else { + // Keep the last FORCE* event for each node, or if there is none the last normal event + Map last = Maps.newHashMapWithExpectedSize(events.size()); + for (TopologyEvent event : events) { + if (event.isForceEvent() + || !last.containsKey(event.broadcastRpcAddress) + || !last.get(event.broadcastRpcAddress).isForceEvent()) { + last.put(event.broadcastRpcAddress, event); + } + } + result = last.values(); + } + LOG.debug("[{}] Coalesced topology events: {} => {}", logPrefix, events, result); + return result; + } + + // Called when the debouncer flushes + private void flushTopologyEvents(Collection events) { + assert adminExecutor.inEventLoop(); + for (TopologyEvent event : events) { + onDebouncedTopologyEvent(event); + } + } + + private void close() { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + return; + } + closeWasCalled = true; + topologyEventDebouncer.stop(); + closeFuture.complete(null); + } + + private void setState(DefaultNode node, NodeState newState, String reason) { + NodeState oldState = node.state; + if (oldState != newState) { + LOG.debug( + "[{}] Transitioning {} {}=>{} (because {})", + logPrefix, + node, + oldState, + newState, + reason); + node.state = newState; + if (newState == NodeState.UP) { + node.upSinceMillis = System.currentTimeMillis(); + } else { + node.upSinceMillis = -1; + } + // Fire the state change event, either immediately, or after a refresh if the node just came + // back up. + // If oldState == UNKNOWN, the node was just added, we already refreshed while processing + // the addition. + if (oldState == NodeState.UNKNOWN || newState != NodeState.UP) { + eventBus.fire(NodeStateEvent.changed(oldState, newState, node)); + } else { + metadataManager + .refreshNode(node) + .whenComplete( + (success, error) -> { + try { + if (error != null) { + LOG.debug( + "[{}] Error while refreshing info for {}", logPrefix, node, error); + } + // Fire the event whether the refresh succeeded or not + eventBus.fire(NodeStateEvent.changed(oldState, newState, node)); + } catch (Throwable t) { + Loggers.warnWithException(LOG, "[{}] Unexpected exception", logPrefix, t); + } + }); + } + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodesRefresh.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodesRefresh.java new file mode 100644 index 00000000000..04817eaa2ae --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NodesRefresh.java @@ -0,0 +1,64 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.internal.core.metadata.token.TokenFactory; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.Collections; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +abstract class NodesRefresh implements MetadataRefresh { + + private static final Logger LOG = LoggerFactory.getLogger(NodesRefresh.class); + + /** + * @return whether the node's token have changed as a result of this operation (unfortunately we + * mutate the tokens in-place, so there is no way to check this after the fact). + */ + protected static boolean copyInfos( + NodeInfo nodeInfo, DefaultNode node, TokenFactory tokenFactory, String logPrefix) { + node.broadcastRpcAddress = nodeInfo.getBroadcastRpcAddress().orElse(null); + node.broadcastAddress = nodeInfo.getBroadcastAddress().orElse(null); + node.listenAddress = nodeInfo.getListenAddress().orElse(null); + node.datacenter = nodeInfo.getDatacenter(); + node.rack = nodeInfo.getRack(); + node.hostId = nodeInfo.getHostId(); + node.schemaVersion = nodeInfo.getSchemaVersion(); + String versionString = nodeInfo.getCassandraVersion(); + try { + node.cassandraVersion = Version.parse(versionString); + } catch (IllegalArgumentException e) { + LOG.warn( + "[{}] Error converting Cassandra version '{}' for {}", + logPrefix, + versionString, + node.getEndPoint()); + } + boolean tokensChanged = tokenFactory != null && !node.rawTokens.equals(nodeInfo.getTokens()); + if (tokensChanged) { + node.rawTokens = nodeInfo.getTokens(); + } + node.extras = + (nodeInfo.getExtras() == null) + ? Collections.emptyMap() + : ImmutableMap.copyOf(nodeInfo.getExtras()); + return tokensChanged; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NoopNodeStateListener.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NoopNodeStateListener.java new file mode 100644 index 00000000000..2e70d8efb6a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/NoopNodeStateListener.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.NodeStateListenerBase; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import net.jcip.annotations.ThreadSafe; + +/** + * Default node state listener implementation with empty methods. + * + *

To activate this listener, modify the {@code advanced.node-state-listener} section in the + * driver configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.node-state-listener {
+ *     class = NoopNodeStateListener
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + * + *

Note that if a listener is specified programmatically with {@link + * SessionBuilder#withNodeStateListener(NodeStateListener)}, the configuration is ignored. + */ +@ThreadSafe +public class NoopNodeStateListener extends NodeStateListenerBase { + + public NoopNodeStateListener(@SuppressWarnings("unused") DriverContext context) { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/RemoveNodeRefresh.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/RemoveNodeRefresh.java new file mode 100644 index 00000000000..7ee741664ef --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/RemoveNodeRefresh.java @@ -0,0 +1,73 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.net.InetSocketAddress; +import java.util.Map; +import java.util.UUID; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class RemoveNodeRefresh extends NodesRefresh { + + private static final Logger LOG = LoggerFactory.getLogger(RemoveNodeRefresh.class); + + @VisibleForTesting final InetSocketAddress broadcastRpcAddressToRemove; + + RemoveNodeRefresh(InetSocketAddress broadcastRpcAddressToRemove) { + this.broadcastRpcAddressToRemove = broadcastRpcAddressToRemove; + } + + @Override + public Result compute( + DefaultMetadata oldMetadata, boolean tokenMapEnabled, InternalDriverContext context) { + + String logPrefix = context.getSessionName(); + + Map oldNodes = oldMetadata.getNodes(); + + ImmutableMap.Builder newNodesBuilder = ImmutableMap.builder(); + Node removedNode = null; + for (Node node : oldNodes.values()) { + if (node.getBroadcastRpcAddress().isPresent() + && node.getBroadcastRpcAddress().get().equals(broadcastRpcAddressToRemove)) { + removedNode = node; + } else { + assert node.getHostId() != null; // nodes in metadata.getNodes() always have their id set + newNodesBuilder.put(node.getHostId(), node); + } + } + + if (removedNode == null) { + // This should never happen because we already check the event in NodeStateManager, but handle + // just in case. + LOG.debug("[{}] Couldn't find node {} to remove", broadcastRpcAddressToRemove); + return new Result(oldMetadata); + } else { + LOG.debug("[{}] Removing node {}", logPrefix, removedNode); + return new Result( + oldMetadata.withNodes(newNodesBuilder.build(), tokenMapEnabled, false, null, context), + ImmutableList.of(NodeStateEvent.removed((DefaultNode) removedNode))); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/SchemaAgreementChecker.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/SchemaAgreementChecker.java new file mode 100644 index 00000000000..5ddd2f0806e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/SchemaAgreementChecker.java @@ -0,0 +1,219 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRequestHandler; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.util.NanoTime; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +class SchemaAgreementChecker { + + private static final Logger LOG = LoggerFactory.getLogger(SchemaAgreementChecker.class); + private static final int INFINITE_PAGE_SIZE = -1; + @VisibleForTesting static final InetAddress BIND_ALL_ADDRESS; + + static { + try { + BIND_ALL_ADDRESS = InetAddress.getByAddress(new byte[4]); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + private final DriverChannel channel; + private final InternalDriverContext context; + private final int port; + private final String logPrefix; + private final Duration queryTimeout; + private final long intervalNs; + private final long timeoutNs; + private final boolean warnOnFailure; + private final long start; + private final CompletableFuture result = new CompletableFuture<>(); + + SchemaAgreementChecker( + DriverChannel channel, InternalDriverContext context, int port, String logPrefix) { + this.channel = channel; + this.context = context; + this.port = port; + this.logPrefix = logPrefix; + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + this.queryTimeout = config.getDuration(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT); + this.intervalNs = + config.getDuration(DefaultDriverOption.CONTROL_CONNECTION_AGREEMENT_INTERVAL).toNanos(); + this.timeoutNs = + config.getDuration(DefaultDriverOption.CONTROL_CONNECTION_AGREEMENT_TIMEOUT).toNanos(); + this.warnOnFailure = config.getBoolean(DefaultDriverOption.CONTROL_CONNECTION_AGREEMENT_WARN); + this.start = System.nanoTime(); + } + + public CompletionStage run() { + LOG.debug("[{}] Checking schema agreement", logPrefix); + if (timeoutNs == 0) { + result.complete(false); + } else { + sendQueries(); + } + return result; + } + + private void sendQueries() { + long elapsedNs = System.nanoTime() - start; + if (elapsedNs > timeoutNs) { + String message = + String.format( + "[%s] Schema agreement not reached after %s", logPrefix, NanoTime.format(elapsedNs)); + if (warnOnFailure) { + LOG.warn(message); + } else { + LOG.debug(message); + } + result.complete(false); + } else { + CompletionStage localQuery = + query("SELECT schema_version FROM system.local WHERE key='local'"); + CompletionStage peersQuery = + query("SELECT host_id, schema_version FROM system.peers"); + + localQuery + .thenCombine(peersQuery, this::extractSchemaVersions) + .whenComplete(this::completeOrReschedule); + } + } + + private Set extractSchemaVersions(AdminResult controlNodeResult, AdminResult peersResult) { + // Gather the versions of all the nodes that are UP + ImmutableSet.Builder schemaVersions = ImmutableSet.builder(); + + // Control node (implicitly UP, we've just queried it) + Iterator iterator = controlNodeResult.iterator(); + if (iterator.hasNext()) { + AdminRow localRow = iterator.next(); + UUID schemaVersion = localRow.getUuid("schema_version"); + if (schemaVersion == null) { + LOG.warn( + "[{}] Missing schema_version for control node {}, " + + "excluding from schema agreement check", + logPrefix, + channel.getEndPoint()); + } else { + schemaVersions.add(schemaVersion); + } + } else { + LOG.warn( + "[{}] Missing system.local row for control node {}, " + + "excluding from schema agreement check", + logPrefix, + channel.getEndPoint()); + } + + Map nodes = context.getMetadataManager().getMetadata().getNodes(); + for (AdminRow peerRow : peersResult) { + UUID hostId = peerRow.getUuid("host_id"); + if (hostId == null) { + LOG.warn( + "[{}] Missing host_id in system.peers row, excluding from schema agreement check", + logPrefix); + continue; + } + UUID schemaVersion = peerRow.getUuid("schema_version"); + if (schemaVersion == null) { + LOG.warn( + "[{}] Missing schema_version in system.peers row for {}, " + + "excluding from schema agreement check", + logPrefix, + hostId); + continue; + } + Node node = nodes.get(hostId); + if (node == null) { + LOG.warn("[{}] Unknown peer {}, excluding from schema agreement check", logPrefix, hostId); + continue; + } else if (node.getState() != NodeState.UP) { + LOG.debug("[{}] Peer {} is down, excluding from schema agreement check", logPrefix, hostId); + continue; + } + schemaVersions.add(schemaVersion); + } + return schemaVersions.build(); + } + + private void completeOrReschedule(Set uuids, Throwable error) { + if (error != null) { + LOG.debug( + "[{}] Error while checking schema agreement, completing now (false)", logPrefix, error); + result.complete(false); + } else if (uuids.size() == 1) { + LOG.debug( + "[{}] Schema agreement reached ({}), completing", logPrefix, uuids.iterator().next()); + result.complete(true); + } else { + LOG.debug( + "[{}] Schema agreement not reached yet ({}), rescheduling in {}", + logPrefix, + uuids, + NanoTime.format(intervalNs)); + channel + .eventLoop() + .schedule(this::sendQueries, intervalNs, TimeUnit.NANOSECONDS) + .addListener( + f -> { + if (!f.isSuccess()) { + LOG.debug( + "[{}] Error while rescheduling schema agreement, completing now (false)", + logPrefix, + f.cause()); + } + }); + } + } + + @VisibleForTesting + protected CompletionStage query(String queryString) { + return AdminRequestHandler.query( + channel, + queryString, + Collections.emptyMap(), + queryTimeout, + INFINITE_PAGE_SIZE, + logPrefix) + .start(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/TokensChangedRefresh.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/TokensChangedRefresh.java new file mode 100644 index 00000000000..174ed029d7a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/TokensChangedRefresh.java @@ -0,0 +1,30 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +class TokensChangedRefresh implements MetadataRefresh { + + @Override + public Result compute( + DefaultMetadata oldMetadata, boolean tokenMapEnabled, InternalDriverContext context) { + return new Result( + oldMetadata.withNodes(oldMetadata.getNodes(), tokenMapEnabled, true, null, context)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/TopologyEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/TopologyEvent.java new file mode 100644 index 00000000000..4f11fa4b182 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/TopologyEvent.java @@ -0,0 +1,178 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.metadata.Node; +import java.net.InetSocketAddress; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +/** + * An event emitted from the {@link TopologyMonitor}, indicating a change in the topology of the + * Cassandra cluster. + * + *

Internally, the driver uses this to handle {@code TOPOLOGY_CHANGE} and {@code STATUS_CHANGE} + * events received on the control connection; for historical reasons, those protocol events identify + * nodes by their (untranslated) {@linkplain Node#getBroadcastRpcAddress() broadcast RPC address}. + * + *

As shown by the names, most of these events are mere suggestions, that the driver might choose + * to ignore if they contradict other information it has about the nodes; see the documentation of + * each factory method for detailed explanations. + */ +@Immutable +public class TopologyEvent { + + public enum Type { + SUGGEST_UP, + SUGGEST_DOWN, + FORCE_UP, + FORCE_DOWN, + SUGGEST_ADDED, + SUGGEST_REMOVED, + } + + /** + * Suggests that a node is up. + * + *

    + *
  • if the node is currently ignored by the driver's load balancing policy, this is reflected + * in the driver metadata's corresponding {@link Node}, for information purposes only. + *
  • otherwise: + *
      + *
    • if the driver already had active connections to that node, this has no effect. + *
    • if the driver was currently reconnecting to the node, this causes the current + * {@link + * com.datastax.oss.driver.api.core.connection.ReconnectionPolicy.ReconnectionSchedule} + * to be reset, and the next reconnection attempt to happen immediately. + *
    + *
+ */ + public static TopologyEvent suggestUp(InetSocketAddress broadcastRpcAddress) { + return new TopologyEvent(Type.SUGGEST_UP, broadcastRpcAddress); + } + + /** + * Suggests that a node is down. + * + *
    + *
  • if the node is currently ignored by the driver's load balancing policy, this is reflected + * in the driver metadata's corresponding {@link Node}, for information purposes only. + *
  • otherwise, if the driver still has at least one active connection to that node, this is + * ignored. In other words, a functioning connection is considered a more reliable + * indication than a topology event. + *

    If you want to bypass that behavior and force the node down, use {@link + * #forceDown(InetSocketAddress)}. + *

+ */ + public static TopologyEvent suggestDown(InetSocketAddress broadcastRpcAddress) { + return new TopologyEvent(Type.SUGGEST_DOWN, broadcastRpcAddress); + } + + /** + * Forces the driver to set a node down. + * + *
    + *
  • if the node is currently ignored by the driver's load balancing policy, this is reflected + * in the driver metadata, for information purposes only. + *
  • otherwise, all active connections to the node are closed, and any active reconnection is + * cancelled. + *
+ * + * In all cases, the driver will never try to reconnect to the node again. If you decide to + * reconnect to it later, use {@link #forceUp(InetSocketAddress)}. + * + *

This is intended for deployments that use a custom {@link TopologyMonitor} (for example if + * you do some kind of maintenance on a live node). This is also used internally by the driver + * when it detects an unrecoverable error, such as a node that does not support the current + * protocol version. + */ + public static TopologyEvent forceDown(InetSocketAddress broadcastRpcAddress) { + return new TopologyEvent(Type.FORCE_DOWN, broadcastRpcAddress); + } + + /** + * Cancels a previous {@link #forceDown(InetSocketAddress)} event for the node. + * + *

The node will be set back UP. If it is not ignored by the load balancing policy, a + * connection pool will be reopened. + */ + public static TopologyEvent forceUp(InetSocketAddress broadcastRpcAddress) { + return new TopologyEvent(Type.FORCE_UP, broadcastRpcAddress); + } + + /** + * Suggests that a new node was added in the cluster. + * + *

The driver will ignore this event if the node is already present in its metadata, or if + * information about the node can't be refreshed (i.e. {@link + * TopologyMonitor#getNewNodeInfo(InetSocketAddress)} fails). + */ + public static TopologyEvent suggestAdded(InetSocketAddress broadcastRpcAddress) { + return new TopologyEvent(Type.SUGGEST_ADDED, broadcastRpcAddress); + } + + /** + * Suggests that a node was removed from the cluster. + * + *

The driver ignore this event if the node does not exist in its metadata. + */ + public static TopologyEvent suggestRemoved(InetSocketAddress broadcastRpcAddress) { + return new TopologyEvent(Type.SUGGEST_REMOVED, broadcastRpcAddress); + } + + public final Type type; + + /** + * Note that this is the untranslated broadcast RPC address, as it was received in the + * protocol event. + * + * @see Node#getBroadcastRpcAddress() + */ + public final InetSocketAddress broadcastRpcAddress; + + /** Builds a new instance (the static methods in this class are a preferred alternative). */ + public TopologyEvent(Type type, InetSocketAddress broadcastRpcAddress) { + this.type = type; + this.broadcastRpcAddress = broadcastRpcAddress; + } + + public boolean isForceEvent() { + return type == Type.FORCE_DOWN || type == Type.FORCE_UP; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof TopologyEvent) { + TopologyEvent that = (TopologyEvent) other; + return this.type == that.type + && Objects.equals(this.broadcastRpcAddress, that.broadcastRpcAddress); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(this.type, this.broadcastRpcAddress); + } + + @Override + public String toString() { + return "TopologyEvent(" + type + ", " + broadcastRpcAddress + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/TopologyMonitor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/TopologyMonitor.java new file mode 100644 index 00000000000..d01ae3d954f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/TopologyMonitor.java @@ -0,0 +1,117 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.api.core.AsyncAutoCloseable; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import java.net.InetSocketAddress; +import java.util.Optional; +import java.util.concurrent.CompletionStage; + +/** + * Monitors the state of the Cassandra cluster. + * + *

It can either push {@link TopologyEvent topology events} to the rest of the driver (to do + * that, retrieve the {@link EventBus}) from the {@link InternalDriverContext}), or receive requests + * to refresh data about the nodes. + * + *

The default implementation uses the control connection: {@code TOPOLOGY_CHANGE} and {@code + * STATUS_CHANGE} events on the connection are converted into {@code TopologyEvent}s, and node + * refreshes are done with queries to system tables. If you prefer to rely on an external monitoring + * tool, this can be completely overridden. + */ +public interface TopologyMonitor extends AsyncAutoCloseable { + + /** + * Triggers the initialization of the monitor. + * + *

The completion of the future returned by this method marks the point when the driver + * considers itself "connected" to the cluster, and proceeds with the rest of the initialization: + * refreshing the list of nodes and the metadata, opening connection pools, etc. + * + *

If {@code advanced.reconnect-on-init = true} in the configuration, this method is + * responsible for handling reconnection. That is, if the initial attempt to "connect" to the + * cluster fails, it must schedule reattempts, and only complete the returned future when + * connection eventually succeeds. If the user cancels the returned future, then the reconnection + * attempts should stop. + * + *

If this method is called multiple times, it should trigger initialization only once, and + * return the same future on subsequent invocations. + */ + CompletionStage init(); + + /** + * The future returned by {@link #init()}. + * + *

Note that this method may be called before {@link #init()}; at that stage, the future should + * already exist, but be incomplete. + */ + CompletionStage initFuture(); + + /** + * Invoked when the driver needs to refresh the information about an existing node. This is called + * when the node was back and comes back up. + * + *

This will be invoked directly from a driver's internal thread; if the refresh involves + * blocking I/O or heavy computations, it should be scheduled on a separate thread. + * + * @param node the node to refresh. + * @return a future that completes with the information. If the monitor can't fulfill the request + * at this time, it should reply with {@link Optional#empty()}, and the driver will carry on + * with its current information. + */ + CompletionStage> refreshNode(Node node); + + /** + * Invoked when the driver needs to get information about a newly discovered node. + * + *

This will be invoked directly from a driver's internal thread; if the refresh involves + * blocking I/O or heavy computations, it should be scheduled on a separate thread. + * + * @param broadcastRpcAddress the node's broadcast RPC address,. + * @return a future that completes with the information. If the monitor doesn't know any node with + * this address, it should reply with {@link Optional#empty()}; the new node will be ignored. + * @see Node#getBroadcastRpcAddress() + */ + CompletionStage> getNewNodeInfo(InetSocketAddress broadcastRpcAddress); + + /** + * Invoked when the driver needs to refresh information about all the nodes. + * + *

This will be invoked directly from a driver's internal thread; if the refresh involves + * blocking I/O or heavy computations, it should be scheduled on a separate thread. + * + *

The driver calls this at initialization, and uses the result to initialize the {@link + * LoadBalancingPolicy}; successful initialization of the {@link Session} object depends on that + * initial call succeeding. + * + * @return a future that completes with the information. We assume that the full node list will + * always be returned in a single message (no paging). + */ + CompletionStage> refreshNodeList(); + + /** + * Checks whether the nodes in the cluster agree on a common schema version. + * + *

This should typically be implemented with a few retries and a timeout, as the schema can + * take a while to replicate across nodes. + */ + CompletionStage checkSchemaAgreement(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultAggregateMetadata.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultAggregateMetadata.java new file mode 100644 index 00000000000..c1d62ca0d1d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultAggregateMetadata.java @@ -0,0 +1,163 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.AggregateMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import java.util.Optional; +import net.jcip.annotations.Immutable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Immutable +public class DefaultAggregateMetadata implements AggregateMetadata { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultAggregateMetadata.class); + + @NonNull private final CqlIdentifier keyspace; + @NonNull private final FunctionSignature signature; + @Nullable private final FunctionSignature finalFuncSignature; + @Nullable private final Object initCond; + @NonNull private final DataType returnType; + @NonNull private final FunctionSignature stateFuncSignature; + @NonNull private final DataType stateType; + @NonNull private final TypeCodec stateTypeCodec; + + public DefaultAggregateMetadata( + @NonNull CqlIdentifier keyspace, + @NonNull FunctionSignature signature, + @Nullable FunctionSignature finalFuncSignature, + @Nullable Object initCond, + @NonNull DataType returnType, + @NonNull FunctionSignature stateFuncSignature, + @NonNull DataType stateType, + @NonNull TypeCodec stateTypeCodec) { + this.keyspace = keyspace; + this.signature = signature; + this.finalFuncSignature = finalFuncSignature; + this.initCond = initCond; + this.returnType = returnType; + this.stateFuncSignature = stateFuncSignature; + this.stateType = stateType; + this.stateTypeCodec = stateTypeCodec; + } + + @NonNull + @Override + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + @Override + public FunctionSignature getSignature() { + return signature; + } + + @NonNull + @Override + public Optional getFinalFuncSignature() { + return Optional.ofNullable(finalFuncSignature); + } + + @NonNull + @Override + public Optional getInitCond() { + return Optional.ofNullable(initCond); + } + + @NonNull + @Override + public DataType getReturnType() { + return returnType; + } + + @NonNull + @Override + public FunctionSignature getStateFuncSignature() { + return stateFuncSignature; + } + + @NonNull + @Override + public DataType getStateType() { + return stateType; + } + + @NonNull + @Override + public Optional formatInitCond() { + if (initCond == null) { + return Optional.empty(); + } + try { + return Optional.of(stateTypeCodec.format(initCond)); + } catch (Throwable t) { + LOG.warn( + String.format( + "Failed to format INITCOND for %s.%s, using toString instead", + keyspace.asInternal(), signature.getName().asInternal())); + return Optional.of(initCond.toString()); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof AggregateMetadata) { + AggregateMetadata that = (AggregateMetadata) other; + return Objects.equals(this.keyspace, that.getKeyspace()) + && Objects.equals(this.signature, that.getSignature()) + && Objects.equals(this.finalFuncSignature, that.getFinalFuncSignature().orElse(null)) + && Objects.equals(this.initCond, that.getInitCond().orElse(null)) + && Objects.equals(this.returnType, that.getReturnType()) + && Objects.equals(this.stateFuncSignature, that.getStateFuncSignature()) + && Objects.equals(this.stateType, that.getStateType()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash( + keyspace, + signature, + finalFuncSignature, + initCond, + returnType, + stateFuncSignature, + stateType); + } + + @Override + public String toString() { + return "DefaultAggregateMetadata@" + + Integer.toHexString(hashCode()) + + "(" + + keyspace.asInternal() + + "." + + signature + + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultColumnMetadata.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultColumnMetadata.java new file mode 100644 index 00000000000..aecb40e7329 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultColumnMetadata.java @@ -0,0 +1,110 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; +import com.datastax.oss.driver.api.core.type.DataType; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultColumnMetadata implements ColumnMetadata { + @NonNull private final CqlIdentifier keyspace; + @NonNull private final CqlIdentifier parent; + @NonNull private final CqlIdentifier name; + @NonNull private final DataType dataType; + private final boolean isStatic; + + public DefaultColumnMetadata( + @NonNull CqlIdentifier keyspace, + @NonNull CqlIdentifier parent, + @NonNull CqlIdentifier name, + @NonNull DataType dataType, + boolean isStatic) { + this.keyspace = keyspace; + this.parent = parent; + this.name = name; + this.dataType = dataType; + this.isStatic = isStatic; + } + + @NonNull + @Override + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + @Override + public CqlIdentifier getParent() { + return parent; + } + + @NonNull + @Override + public CqlIdentifier getName() { + return name; + } + + @NonNull + @Override + public DataType getType() { + return dataType; + } + + @Override + public boolean isStatic() { + return isStatic; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof ColumnMetadata) { + ColumnMetadata that = (ColumnMetadata) other; + return Objects.equals(this.keyspace, that.getKeyspace()) + && Objects.equals(this.parent, that.getParent()) + && Objects.equals(this.name, that.getName()) + && Objects.equals(this.dataType, that.getType()) + && this.isStatic == that.isStatic(); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(keyspace, parent, name, dataType, isStatic); + } + + @Override + public String toString() { + return "DefaultColumnMetadata@" + + Integer.toHexString(hashCode()) + + "(" + + keyspace.asInternal() + + "." + + parent.asInternal() + + "." + + name.asInternal() + + " " + + dataType.asCql(true, false) + + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultFunctionMetadata.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultFunctionMetadata.java new file mode 100644 index 00000000000..bfed800046d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultFunctionMetadata.java @@ -0,0 +1,136 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultFunctionMetadata implements FunctionMetadata { + + @NonNull private final CqlIdentifier keyspace; + @NonNull private final FunctionSignature signature; + @NonNull private final List parameterNames; + @NonNull private final String body; + private final boolean calledOnNullInput; + @NonNull private final String language; + @NonNull private final DataType returnType; + + public DefaultFunctionMetadata( + @NonNull CqlIdentifier keyspace, + @NonNull FunctionSignature signature, + @NonNull List parameterNames, + @NonNull String body, + boolean calledOnNullInput, + @NonNull String language, + @NonNull DataType returnType) { + Preconditions.checkArgument( + signature.getParameterTypes().size() == parameterNames.size(), + "Number of parameter names should match number of types in the signature (got %s and %s)", + parameterNames.size(), + signature.getParameterTypes().size()); + this.keyspace = keyspace; + this.signature = signature; + this.parameterNames = parameterNames; + this.body = body; + this.calledOnNullInput = calledOnNullInput; + this.language = language; + this.returnType = returnType; + } + + @NonNull + @Override + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + @Override + public FunctionSignature getSignature() { + return signature; + } + + @NonNull + @Override + public List getParameterNames() { + return parameterNames; + } + + @NonNull + @Override + public String getBody() { + return body; + } + + @Override + public boolean isCalledOnNullInput() { + return calledOnNullInput; + } + + @NonNull + @Override + public String getLanguage() { + return language; + } + + @NonNull + @Override + public DataType getReturnType() { + return returnType; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof FunctionMetadata) { + FunctionMetadata that = (FunctionMetadata) other; + return Objects.equals(this.keyspace, that.getKeyspace()) + && Objects.equals(this.signature, that.getSignature()) + && Objects.equals(this.parameterNames, that.getParameterNames()) + && Objects.equals(this.body, that.getBody()) + && this.calledOnNullInput == that.isCalledOnNullInput() + && Objects.equals(this.language, that.getLanguage()) + && Objects.equals(this.returnType, that.getReturnType()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash( + keyspace, signature, parameterNames, body, calledOnNullInput, language, returnType); + } + + @Override + public String toString() { + return "DefaultFunctionMetadata@" + + Integer.toHexString(hashCode()) + + "(" + + keyspace.asInternal() + + "." + + signature + + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultIndexMetadata.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultIndexMetadata.java new file mode 100644 index 00000000000..3fbaeff34b6 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultIndexMetadata.java @@ -0,0 +1,121 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.IndexKind; +import com.datastax.oss.driver.api.core.metadata.schema.IndexMetadata; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultIndexMetadata implements IndexMetadata { + + @NonNull private final CqlIdentifier keyspace; + @NonNull private final CqlIdentifier table; + @NonNull private final CqlIdentifier name; + @NonNull private final IndexKind kind; + @NonNull private final String target; + @NonNull private final Map options; + + public DefaultIndexMetadata( + @NonNull CqlIdentifier keyspace, + @NonNull CqlIdentifier table, + @NonNull CqlIdentifier name, + @NonNull IndexKind kind, + @NonNull String target, + @NonNull Map options) { + this.keyspace = keyspace; + this.table = table; + this.name = name; + this.kind = kind; + this.target = target; + this.options = options; + } + + @NonNull + @Override + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + @Override + public CqlIdentifier getTable() { + return table; + } + + @NonNull + @Override + public CqlIdentifier getName() { + return name; + } + + @NonNull + @Override + public IndexKind getKind() { + return kind; + } + + @NonNull + @Override + public String getTarget() { + return target; + } + + @NonNull + @Override + public Map getOptions() { + return options; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof IndexMetadata) { + IndexMetadata that = (IndexMetadata) other; + return Objects.equals(this.keyspace, that.getKeyspace()) + && Objects.equals(this.table, that.getTable()) + && Objects.equals(this.name, that.getName()) + && Objects.equals(this.kind, that.getKind()) + && Objects.equals(this.target, that.getTarget()) + && Objects.equals(this.options, that.getOptions()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(keyspace, table, name, kind, target, options); + } + + @Override + public String toString() { + return "DefaultIndexMetadata@" + + Integer.toHexString(hashCode()) + + "(" + + keyspace.asInternal() + + "." + + table.asInternal() + + "." + + name.asInternal() + + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultKeyspaceMetadata.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultKeyspaceMetadata.java new file mode 100644 index 00000000000..cb354b583ed --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultKeyspaceMetadata.java @@ -0,0 +1,151 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.AggregateMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.ViewMetadata; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultKeyspaceMetadata implements KeyspaceMetadata { + + @NonNull private final CqlIdentifier name; + private final boolean durableWrites; + private final boolean virtual; + @NonNull private final Map replication; + @NonNull private final Map types; + @NonNull private final Map tables; + @NonNull private final Map views; + @NonNull private final Map functions; + @NonNull private final Map aggregates; + + public DefaultKeyspaceMetadata( + @NonNull CqlIdentifier name, + boolean durableWrites, + boolean virtual, + @NonNull Map replication, + @NonNull Map types, + @NonNull Map tables, + @NonNull Map views, + @NonNull Map functions, + @NonNull Map aggregates) { + this.name = name; + this.durableWrites = durableWrites; + this.virtual = virtual; + this.replication = replication; + this.types = types; + this.tables = tables; + this.views = views; + this.functions = functions; + this.aggregates = aggregates; + } + + @NonNull + @Override + public CqlIdentifier getName() { + return name; + } + + @Override + public boolean isDurableWrites() { + return durableWrites; + } + + @Override + public boolean isVirtual() { + return virtual; + } + + @NonNull + @Override + public Map getReplication() { + return replication; + } + + @NonNull + @Override + public Map getUserDefinedTypes() { + return types; + } + + @NonNull + @Override + public Map getTables() { + return tables; + } + + @NonNull + @Override + public Map getViews() { + return views; + } + + @NonNull + @Override + public Map getFunctions() { + return functions; + } + + @NonNull + @Override + public Map getAggregates() { + return aggregates; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof KeyspaceMetadata) { + KeyspaceMetadata that = (KeyspaceMetadata) other; + return Objects.equals(this.name, that.getName()) + && this.durableWrites == that.isDurableWrites() + && this.virtual == that.isVirtual() + && Objects.equals(this.replication, that.getReplication()) + && Objects.equals(this.types, that.getUserDefinedTypes()) + && Objects.equals(this.tables, that.getTables()) + && Objects.equals(this.views, that.getViews()) + && Objects.equals(this.functions, that.getFunctions()) + && Objects.equals(this.aggregates, that.getAggregates()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash( + name, durableWrites, virtual, replication, types, tables, views, functions, aggregates); + } + + @Override + public String toString() { + return "DefaultKeyspaceMetadata@" + + Integer.toHexString(hashCode()) + + "(" + + name.asInternal() + + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultTableMetadata.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultTableMetadata.java new file mode 100644 index 00000000000..479067ce0ba --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultTableMetadata.java @@ -0,0 +1,172 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.IndexMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultTableMetadata implements TableMetadata { + + @NonNull private final CqlIdentifier keyspace; + @NonNull private final CqlIdentifier name; + // null for virtual tables + @Nullable private final UUID id; + private final boolean compactStorage; + private final boolean virtual; + @NonNull private final List partitionKey; + @NonNull private final Map clusteringColumns; + @NonNull private final Map columns; + @NonNull private final Map options; + @NonNull private final Map indexes; + + public DefaultTableMetadata( + @NonNull CqlIdentifier keyspace, + @NonNull CqlIdentifier name, + @Nullable UUID id, + boolean compactStorage, + boolean virtual, + @NonNull List partitionKey, + @NonNull Map clusteringColumns, + @NonNull Map columns, + @NonNull Map options, + @NonNull Map indexes) { + this.keyspace = keyspace; + this.name = name; + this.id = id; + this.compactStorage = compactStorage; + this.virtual = virtual; + this.partitionKey = partitionKey; + this.clusteringColumns = clusteringColumns; + this.columns = columns; + this.options = options; + this.indexes = indexes; + } + + @NonNull + @Override + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + @Override + public CqlIdentifier getName() { + return name; + } + + @NonNull + @Override + public Optional getId() { + return Optional.ofNullable(id); + } + + @Override + public boolean isCompactStorage() { + return compactStorage; + } + + @Override + public boolean isVirtual() { + return virtual; + } + + @NonNull + @Override + public List getPartitionKey() { + return partitionKey; + } + + @NonNull + @Override + public Map getClusteringColumns() { + return clusteringColumns; + } + + @NonNull + @Override + public Map getColumns() { + return columns; + } + + @NonNull + @Override + public Map getOptions() { + return options; + } + + @NonNull + @Override + public Map getIndexes() { + return indexes; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof TableMetadata) { + TableMetadata that = (TableMetadata) other; + return Objects.equals(this.keyspace, that.getKeyspace()) + && Objects.equals(this.name, that.getName()) + && Objects.equals(Optional.ofNullable(this.id), that.getId()) + && this.compactStorage == that.isCompactStorage() + && this.virtual == that.isVirtual() + && Objects.equals(this.partitionKey, that.getPartitionKey()) + && Objects.equals(this.clusteringColumns, that.getClusteringColumns()) + && Objects.equals(this.columns, that.getColumns()) + && Objects.equals(this.indexes, that.getIndexes()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash( + keyspace, + name, + id, + compactStorage, + virtual, + partitionKey, + clusteringColumns, + columns, + indexes); + } + + @Override + public String toString() { + return "DefaultTableMetadata@" + + Integer.toHexString(hashCode()) + + "(" + + keyspace.asInternal() + + "." + + name.asInternal() + + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultViewMetadata.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultViewMetadata.java new file mode 100644 index 00000000000..53d50931546 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/DefaultViewMetadata.java @@ -0,0 +1,175 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.ViewMetadata; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultViewMetadata implements ViewMetadata { + + @NonNull private final CqlIdentifier keyspace; + @NonNull private final CqlIdentifier name; + @NonNull private final CqlIdentifier baseTable; + private final boolean includesAllColumns; + @Nullable private final String whereClause; + @NonNull private final UUID id; + @NonNull private final ImmutableList partitionKey; + @NonNull private final ImmutableMap clusteringColumns; + @NonNull private final ImmutableMap columns; + @NonNull private final Map options; + + public DefaultViewMetadata( + @NonNull CqlIdentifier keyspace, + @NonNull CqlIdentifier name, + @NonNull CqlIdentifier baseTable, + boolean includesAllColumns, + @Nullable String whereClause, + @NonNull UUID id, + @NonNull ImmutableList partitionKey, + @NonNull ImmutableMap clusteringColumns, + @NonNull ImmutableMap columns, + @NonNull Map options) { + this.keyspace = keyspace; + this.name = name; + this.baseTable = baseTable; + this.includesAllColumns = includesAllColumns; + this.whereClause = whereClause; + this.id = id; + this.partitionKey = partitionKey; + this.clusteringColumns = clusteringColumns; + this.columns = columns; + this.options = options; + } + + @NonNull + @Override + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + @Override + public CqlIdentifier getName() { + return name; + } + + @NonNull + @Override + public Optional getId() { + return Optional.of(id); + } + + @NonNull + @Override + public CqlIdentifier getBaseTable() { + return baseTable; + } + + @Override + public boolean includesAllColumns() { + return includesAllColumns; + } + + @NonNull + @Override + public Optional getWhereClause() { + return Optional.ofNullable(whereClause); + } + + @NonNull + @Override + public List getPartitionKey() { + return partitionKey; + } + + @NonNull + @Override + public Map getClusteringColumns() { + return clusteringColumns; + } + + @NonNull + @Override + public Map getColumns() { + return columns; + } + + @NonNull + @Override + public Map getOptions() { + return options; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof ViewMetadata) { + ViewMetadata that = (ViewMetadata) other; + return Objects.equals(this.keyspace, that.getKeyspace()) + && Objects.equals(this.name, that.getName()) + && Objects.equals(this.baseTable, that.getBaseTable()) + && this.includesAllColumns == that.includesAllColumns() + && Objects.equals(this.whereClause, that.getWhereClause().orElse(null)) + && Objects.equals(Optional.of(this.id), that.getId()) + && Objects.equals(this.partitionKey, that.getPartitionKey()) + && Objects.equals(this.clusteringColumns, that.getClusteringColumns()) + && Objects.equals(this.columns, that.getColumns()) + && Objects.equals(this.options, that.getOptions()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash( + keyspace, + name, + baseTable, + includesAllColumns, + whereClause, + id, + partitionKey, + clusteringColumns, + columns, + options); + } + + @Override + public String toString() { + return "DefaultViewMetadata@" + + Integer.toHexString(hashCode()) + + "(" + + keyspace.asInternal() + + "." + + name.asInternal() + + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/NoopSchemaChangeListener.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/NoopSchemaChangeListener.java new file mode 100644 index 00000000000..2df3935a80f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/NoopSchemaChangeListener.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListenerBase; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import net.jcip.annotations.ThreadSafe; + +/** + * Default schema change listener implementation with empty methods. + * + *

To activate this listener, modify the {@code advanced.schema-change-listener} section in the + * driver configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.schema-change-listener {
+ *     class = NoopSchemaChangeListener
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + * + *

Note that if a listener is specified programmatically with {@link + * SessionBuilder#withSchemaChangeListener(SchemaChangeListener)}, the configuration is ignored. + */ +@ThreadSafe +public class NoopSchemaChangeListener extends SchemaChangeListenerBase { + + public NoopSchemaChangeListener(@SuppressWarnings("unused") DriverContext context) { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/SchemaChangeType.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/SchemaChangeType.java new file mode 100644 index 00000000000..f27d68923f6 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/SchemaChangeType.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +public enum SchemaChangeType { + CREATED, + UPDATED, + DROPPED, + ; +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/ScriptBuilder.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/ScriptBuilder.java new file mode 100644 index 00000000000..d3c9114b740 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/ScriptBuilder.java @@ -0,0 +1,105 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.Describable; +import com.datastax.oss.driver.shaded.guava.common.base.Strings; +import java.util.function.Consumer; +import net.jcip.annotations.NotThreadSafe; + +/** + * A simple builder that is used internally for the queries of {@link Describable} schema elements. + */ +@NotThreadSafe +public class ScriptBuilder { + private static final int INDENT_SIZE = 4; + + private final boolean pretty; + private final StringBuilder builder = new StringBuilder(); + private int indent; + private boolean isAtLineStart; + private boolean isFirstOption = true; + + public ScriptBuilder(boolean pretty) { + this.pretty = pretty; + } + + public ScriptBuilder append(String s) { + if (pretty && isAtLineStart && indent > 0) { + builder.append(Strings.repeat(" ", indent * INDENT_SIZE)); + } + isAtLineStart = false; + builder.append(s); + return this; + } + + public ScriptBuilder append(CqlIdentifier id) { + append(id.asCql(pretty)); + return this; + } + + public ScriptBuilder newLine() { + if (pretty) { + builder.append('\n'); + } else { + builder.append(' '); + } + isAtLineStart = true; + return this; + } + + public ScriptBuilder forceNewLine(int count) { + builder.append(Strings.repeat("\n", count)); + isAtLineStart = true; + return this; + } + + public ScriptBuilder increaseIndent() { + indent += 1; + return this; + } + + public ScriptBuilder decreaseIndent() { + if (indent > 0) { + indent -= 1; + } + return this; + } + + /** Appends "WITH " the first time it's called, then "AND " the next times. */ + public ScriptBuilder andWith() { + if (isFirstOption) { + append(" WITH "); + isFirstOption = false; + } else { + newLine(); + append("AND "); + } + return this; + } + + public ScriptBuilder forEach(Iterable iterable, Consumer action) { + for (E e : iterable) { + action.accept(e); + } + return this; + } + + public String build() { + return builder.toString(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/ShallowUserDefinedType.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/ShallowUserDefinedType.java new file mode 100644 index 00000000000..21d8124c3de --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/ShallowUserDefinedType.java @@ -0,0 +1,149 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.type.DefaultUserDefinedType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.List; +import net.jcip.annotations.Immutable; + +/** + * A temporary UDT implementation that only contains the keyspace and name. + * + *

When we process a schema refresh that spans multiple UDTs, we can't fully materialize them + * right away, because they might depend on each other and the system table query does not return + * them in topological order. So we do a first pass where UDT field that are also UDTs are resolved + * as instances of this class, then a topological sort, then a second pass to replace all shallow + * definitions by the actual instance (which will be a {@link DefaultUserDefinedType}). + */ +@Immutable +public class ShallowUserDefinedType implements UserDefinedType, Serializable { + + private static final long serialVersionUID = 1; + + private final CqlIdentifier keyspace; + private final CqlIdentifier name; + private final boolean frozen; + + public ShallowUserDefinedType(CqlIdentifier keyspace, CqlIdentifier name, boolean frozen) { + this.keyspace = keyspace; + this.name = name; + this.frozen = frozen; + } + + @Nullable + @Override + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + @Override + public CqlIdentifier getName() { + return name; + } + + @Override + public boolean isFrozen() { + return frozen; + } + + @NonNull + @Override + public List getFieldNames() { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + @Override + public int firstIndexOf(CqlIdentifier id) { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + @Override + public int firstIndexOf(String name) { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + @NonNull + @Override + public List getFieldTypes() { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + @NonNull + @Override + public UserDefinedType copy(boolean newFrozen) { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + @NonNull + @Override + public UdtValue newValue() { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + @NonNull + @Override + public UdtValue newValue(@NonNull Object... fields) { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + @NonNull + @Override + public AttachmentPoint getAttachmentPoint() { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + @Override + public boolean isDetached() { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + private void readObject(ObjectInputStream s) throws IOException { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } + + private void writeObject(ObjectOutputStream s) throws IOException { + throw new UnsupportedOperationException( + "This implementation should only be used internally, this is likely a driver bug"); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/AggregateChangeEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/AggregateChangeEvent.java new file mode 100644 index 00000000000..a3d682f72df --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/AggregateChangeEvent.java @@ -0,0 +1,86 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.events; + +import com.datastax.oss.driver.api.core.metadata.schema.AggregateMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.SchemaChangeType; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class AggregateChangeEvent { + + public static AggregateChangeEvent dropped(AggregateMetadata oldAggregate) { + return new AggregateChangeEvent(SchemaChangeType.DROPPED, oldAggregate, null); + } + + public static AggregateChangeEvent created(AggregateMetadata newAggregate) { + return new AggregateChangeEvent(SchemaChangeType.CREATED, null, newAggregate); + } + + public static AggregateChangeEvent updated( + AggregateMetadata oldAggregate, AggregateMetadata newAggregate) { + return new AggregateChangeEvent(SchemaChangeType.UPDATED, oldAggregate, newAggregate); + } + + public final SchemaChangeType changeType; + /** {@code null} if the event is a creation */ + public final AggregateMetadata oldAggregate; + /** {@code null} if the event is a drop */ + public final AggregateMetadata newAggregate; + + private AggregateChangeEvent( + SchemaChangeType changeType, AggregateMetadata oldAggregate, AggregateMetadata newAggregate) { + this.changeType = changeType; + this.oldAggregate = oldAggregate; + this.newAggregate = newAggregate; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof AggregateChangeEvent) { + AggregateChangeEvent that = (AggregateChangeEvent) other; + return this.changeType == that.changeType + && Objects.equals(this.oldAggregate, that.oldAggregate) + && Objects.equals(this.newAggregate, that.newAggregate); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(changeType, oldAggregate, newAggregate); + } + + @Override + public String toString() { + switch (changeType) { + case CREATED: + return String.format("AggregateChangeEvent(CREATED %s)", newAggregate.getSignature()); + case UPDATED: + return String.format( + "AggregateChangeEvent(UPDATED %s=>%s)", + oldAggregate.getSignature(), newAggregate.getSignature()); + case DROPPED: + return String.format("AggregateChangeEvent(DROPPED %s)", oldAggregate.getSignature()); + default: + throw new IllegalStateException("Unsupported change type " + changeType); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/FunctionChangeEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/FunctionChangeEvent.java new file mode 100644 index 00000000000..8c5da87a30f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/FunctionChangeEvent.java @@ -0,0 +1,86 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.events; + +import com.datastax.oss.driver.api.core.metadata.schema.FunctionMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.SchemaChangeType; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class FunctionChangeEvent { + + public static FunctionChangeEvent dropped(FunctionMetadata oldFunction) { + return new FunctionChangeEvent(SchemaChangeType.DROPPED, oldFunction, null); + } + + public static FunctionChangeEvent created(FunctionMetadata newFunction) { + return new FunctionChangeEvent(SchemaChangeType.CREATED, null, newFunction); + } + + public static FunctionChangeEvent updated( + FunctionMetadata oldFunction, FunctionMetadata newFunction) { + return new FunctionChangeEvent(SchemaChangeType.UPDATED, oldFunction, newFunction); + } + + public final SchemaChangeType changeType; + /** {@code null} if the event is a creation */ + public final FunctionMetadata oldFunction; + /** {@code null} if the event is a drop */ + public final FunctionMetadata newFunction; + + private FunctionChangeEvent( + SchemaChangeType changeType, FunctionMetadata oldFunction, FunctionMetadata newFunction) { + this.changeType = changeType; + this.oldFunction = oldFunction; + this.newFunction = newFunction; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof FunctionChangeEvent) { + FunctionChangeEvent that = (FunctionChangeEvent) other; + return this.changeType == that.changeType + && Objects.equals(this.oldFunction, that.oldFunction) + && Objects.equals(this.newFunction, that.newFunction); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(changeType, oldFunction, newFunction); + } + + @Override + public String toString() { + switch (changeType) { + case CREATED: + return String.format("FunctionChangeEvent(CREATED %s)", newFunction.getSignature()); + case UPDATED: + return String.format( + "FunctionChangeEvent(UPDATED %s=>%s)", + oldFunction.getSignature(), newFunction.getSignature()); + case DROPPED: + return String.format("FunctionChangeEvent(DROPPED %s)", oldFunction.getSignature()); + default: + throw new IllegalStateException("Unsupported change type " + changeType); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/KeyspaceChangeEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/KeyspaceChangeEvent.java new file mode 100644 index 00000000000..ef448934ddd --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/KeyspaceChangeEvent.java @@ -0,0 +1,85 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.events; + +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.SchemaChangeType; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class KeyspaceChangeEvent { + + public static KeyspaceChangeEvent dropped(KeyspaceMetadata oldKeyspace) { + return new KeyspaceChangeEvent(SchemaChangeType.DROPPED, oldKeyspace, null); + } + + public static KeyspaceChangeEvent created(KeyspaceMetadata newKeyspace) { + return new KeyspaceChangeEvent(SchemaChangeType.CREATED, null, newKeyspace); + } + + public static KeyspaceChangeEvent updated( + KeyspaceMetadata oldKeyspace, KeyspaceMetadata newKeyspace) { + return new KeyspaceChangeEvent(SchemaChangeType.UPDATED, oldKeyspace, newKeyspace); + } + + public final SchemaChangeType changeType; + /** {@code null} if the event is a creation */ + public final KeyspaceMetadata oldKeyspace; + /** {@code null} if the event is a drop */ + public final KeyspaceMetadata newKeyspace; + + private KeyspaceChangeEvent( + SchemaChangeType changeType, KeyspaceMetadata oldKeyspace, KeyspaceMetadata newKeyspace) { + this.changeType = changeType; + this.oldKeyspace = oldKeyspace; + this.newKeyspace = newKeyspace; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof KeyspaceChangeEvent) { + KeyspaceChangeEvent that = (KeyspaceChangeEvent) other; + return this.changeType == that.changeType + && Objects.equals(this.oldKeyspace, that.oldKeyspace) + && Objects.equals(this.newKeyspace, that.newKeyspace); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(changeType, oldKeyspace, newKeyspace); + } + + @Override + public String toString() { + switch (changeType) { + case CREATED: + return String.format("KeyspaceChangeEvent(CREATED %s)", newKeyspace.getName()); + case UPDATED: + return String.format( + "KeyspaceChangeEvent(UPDATED %s=>%s)", oldKeyspace.getName(), newKeyspace.getName()); + case DROPPED: + return String.format("KeyspaceChangeEvent(DROPPED %s)", oldKeyspace.getName()); + default: + throw new IllegalStateException("Unsupported change type " + changeType); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/TableChangeEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/TableChangeEvent.java new file mode 100644 index 00000000000..16d67b8f06b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/TableChangeEvent.java @@ -0,0 +1,84 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.events; + +import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.SchemaChangeType; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class TableChangeEvent { + + public static TableChangeEvent dropped(TableMetadata oldTable) { + return new TableChangeEvent(SchemaChangeType.DROPPED, oldTable, null); + } + + public static TableChangeEvent created(TableMetadata newTable) { + return new TableChangeEvent(SchemaChangeType.CREATED, null, newTable); + } + + public static TableChangeEvent updated(TableMetadata oldTable, TableMetadata newTable) { + return new TableChangeEvent(SchemaChangeType.UPDATED, oldTable, newTable); + } + + public final SchemaChangeType changeType; + /** {@code null} if the event is a creation */ + public final TableMetadata oldTable; + /** {@code null} if the event is a drop */ + public final TableMetadata newTable; + + private TableChangeEvent( + SchemaChangeType changeType, TableMetadata oldTable, TableMetadata newTable) { + this.changeType = changeType; + this.oldTable = oldTable; + this.newTable = newTable; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof TableChangeEvent) { + TableChangeEvent that = (TableChangeEvent) other; + return this.changeType == that.changeType + && Objects.equals(this.oldTable, that.oldTable) + && Objects.equals(this.newTable, that.newTable); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(changeType, oldTable, newTable); + } + + @Override + public String toString() { + switch (changeType) { + case CREATED: + return String.format("TableChangeEvent(CREATED %s)", newTable.getName()); + case UPDATED: + return String.format( + "TableChangeEvent(UPDATED %s=>%s)", oldTable.getName(), newTable.getName()); + case DROPPED: + return String.format("TableChangeEvent(DROPPED %s)", oldTable.getName()); + default: + throw new IllegalStateException("Unsupported change type " + changeType); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/TypeChangeEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/TypeChangeEvent.java new file mode 100644 index 00000000000..2575d2e6b45 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/TypeChangeEvent.java @@ -0,0 +1,84 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.events; + +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.metadata.schema.SchemaChangeType; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class TypeChangeEvent { + + public static TypeChangeEvent dropped(UserDefinedType oldType) { + return new TypeChangeEvent(SchemaChangeType.DROPPED, oldType, null); + } + + public static TypeChangeEvent created(UserDefinedType newType) { + return new TypeChangeEvent(SchemaChangeType.CREATED, null, newType); + } + + public static TypeChangeEvent updated(UserDefinedType oldType, UserDefinedType newType) { + return new TypeChangeEvent(SchemaChangeType.UPDATED, oldType, newType); + } + + public final SchemaChangeType changeType; + /** {@code null} if the event is a creation */ + public final UserDefinedType oldType; + /** {@code null} if the event is a drop */ + public final UserDefinedType newType; + + private TypeChangeEvent( + SchemaChangeType changeType, UserDefinedType oldType, UserDefinedType newType) { + this.changeType = changeType; + this.oldType = oldType; + this.newType = newType; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof TypeChangeEvent) { + TypeChangeEvent that = (TypeChangeEvent) other; + return this.changeType == that.changeType + && Objects.equals(this.oldType, that.oldType) + && Objects.equals(this.newType, that.newType); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(changeType, oldType, newType); + } + + @Override + public String toString() { + switch (changeType) { + case CREATED: + return String.format("TypeChangeEvent(CREATED %s)", newType.getName()); + case UPDATED: + return String.format( + "TypeChangeEvent(UPDATED %s=>%s)", oldType.getName(), newType.getName()); + case DROPPED: + return String.format("TypeChangeEvent(DROPPED %s)", oldType.getName()); + default: + throw new IllegalStateException("Unsupported change type " + changeType); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/ViewChangeEvent.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/ViewChangeEvent.java new file mode 100644 index 00000000000..84f6a4d1e61 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/events/ViewChangeEvent.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.events; + +import com.datastax.oss.driver.api.core.metadata.schema.ViewMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.SchemaChangeType; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class ViewChangeEvent { + + public static ViewChangeEvent dropped(ViewMetadata oldView) { + return new ViewChangeEvent(SchemaChangeType.DROPPED, oldView, null); + } + + public static ViewChangeEvent created(ViewMetadata newView) { + return new ViewChangeEvent(SchemaChangeType.CREATED, null, newView); + } + + public static ViewChangeEvent updated(ViewMetadata oldView, ViewMetadata newView) { + return new ViewChangeEvent(SchemaChangeType.UPDATED, oldView, newView); + } + + public final SchemaChangeType changeType; + /** {@code null} if the event is a creation */ + public final ViewMetadata oldView; + /** {@code null} if the event is a drop */ + public final ViewMetadata newView; + + private ViewChangeEvent(SchemaChangeType changeType, ViewMetadata oldView, ViewMetadata newView) { + this.changeType = changeType; + this.oldView = oldView; + this.newView = newView; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof ViewChangeEvent) { + ViewChangeEvent that = (ViewChangeEvent) other; + return this.changeType == that.changeType + && Objects.equals(this.oldView, that.oldView) + && Objects.equals(this.newView, that.newView); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(changeType, oldView, newView); + } + + @Override + public String toString() { + switch (changeType) { + case CREATED: + return String.format("ViewChangeEvent(CREATED %s)", newView.getName()); + case UPDATED: + return String.format( + "ViewChangeEvent(UPDATED %s=>%s)", oldView.getName(), newView.getName()); + case DROPPED: + return String.format("ViewChangeEvent(DROPPED %s)", oldView.getName()); + default: + throw new IllegalStateException("Unsupported change type " + changeType); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/AggregateParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/AggregateParser.java new file mode 100644 index 00000000000..1e74a1ee9a7 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/AggregateParser.java @@ -0,0 +1,123 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.AggregateMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.schema.DefaultAggregateMetadata; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class AggregateParser { + private final DataTypeParser dataTypeParser; + private final InternalDriverContext context; + + public AggregateParser(DataTypeParser dataTypeParser, InternalDriverContext context) { + this.dataTypeParser = dataTypeParser; + this.context = context; + } + + public AggregateMetadata parseAggregate( + AdminRow row, + CqlIdentifier keyspaceId, + Map userDefinedTypes) { + // Cassandra < 3.0: + // CREATE TABLE system.schema_aggregates ( + // keyspace_name text, + // aggregate_name text, + // signature frozen>, + // argument_types list, + // final_func text, + // initcond blob, + // return_type text, + // state_func text, + // state_type text, + // PRIMARY KEY (keyspace_name, aggregate_name, signature) + // ) WITH CLUSTERING ORDER BY (aggregate_name ASC, signature ASC) + // + // Cassandra >= 3.0: + // CREATE TABLE system.schema_aggregates ( + // keyspace_name text, + // aggregate_name text, + // argument_types frozen>, + // final_func text, + // initcond text, + // return_type text, + // state_func text, + // state_type text, + // PRIMARY KEY (keyspace_name, aggregate_name, argument_types) + // ) WITH CLUSTERING ORDER BY (aggregate_name ASC, argument_types ASC) + String simpleName = row.getString("aggregate_name"); + List argumentTypes = row.getListOfString("argument_types"); + FunctionSignature signature = + new FunctionSignature( + CqlIdentifier.fromInternal(simpleName), + dataTypeParser.parse(keyspaceId, argumentTypes, userDefinedTypes, context)); + + DataType stateType = + dataTypeParser.parse(keyspaceId, row.getString("state_type"), userDefinedTypes, context); + TypeCodec stateTypeCodec = context.getCodecRegistry().codecFor(stateType); + + String stateFuncSimpleName = row.getString("state_func"); + FunctionSignature stateFuncSignature = + new FunctionSignature( + CqlIdentifier.fromInternal(stateFuncSimpleName), + ImmutableList.builder() + .add(stateType) + .addAll(signature.getParameterTypes()) + .build()); + + String finalFuncSimpleName = row.getString("final_func"); + FunctionSignature finalFuncSignature = + (finalFuncSimpleName == null) + ? null + : new FunctionSignature(CqlIdentifier.fromInternal(finalFuncSimpleName), stateType); + + DataType returnType = + dataTypeParser.parse(keyspaceId, row.getString("return_type"), userDefinedTypes, context); + + Object initCond; + if (row.isString("initcond")) { // Cassandra 3 + String initCondString = row.getString("initcond"); + initCond = (initCondString == null) ? null : stateTypeCodec.parse(initCondString); + } else { // Cassandra 2.2 + ByteBuffer initCondBlob = row.getByteBuffer("initcond"); + initCond = + (initCondBlob == null) + ? null + : stateTypeCodec.decode(initCondBlob, context.getProtocolVersion()); + } + return new DefaultAggregateMetadata( + keyspaceId, + signature, + finalFuncSignature, + initCond, + returnType, + stateFuncSignature, + stateType, + stateTypeCodec); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/CassandraSchemaParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/CassandraSchemaParser.java new file mode 100644 index 00000000000..04b6f69edec --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/CassandraSchemaParser.java @@ -0,0 +1,223 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.AggregateMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.ViewMetadata; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.schema.DefaultKeyspaceMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; +import com.datastax.oss.driver.internal.core.metadata.schema.refresh.SchemaRefresh; +import com.datastax.oss.driver.internal.core.util.NanoTime; +import com.datastax.oss.driver.shaded.guava.common.base.MoreObjects; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.Collections; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Default parser implementation for Cassandra. + * + *

For modularity, the code for each element row is split into separate classes (schema stuff is + * not on the hot path, so creating a few extra objects doesn't matter). + */ +@ThreadSafe +public class CassandraSchemaParser implements SchemaParser { + + private static final Logger LOG = LoggerFactory.getLogger(CassandraSchemaParser.class); + + private final SchemaRows rows; + private final UserDefinedTypeParser userDefinedTypeParser; + private final TableParser tableParser; + private final ViewParser viewParser; + private final FunctionParser functionParser; + private final AggregateParser aggregateParser; + private final String logPrefix; + private final long startTimeNs = System.nanoTime(); + + public CassandraSchemaParser(SchemaRows rows, InternalDriverContext context) { + this.rows = rows; + this.logPrefix = context.getSessionName(); + + this.userDefinedTypeParser = new UserDefinedTypeParser(rows.dataTypeParser(), context); + this.tableParser = new TableParser(rows, context); + this.viewParser = new ViewParser(rows, context); + this.functionParser = new FunctionParser(rows.dataTypeParser(), context); + this.aggregateParser = new AggregateParser(rows.dataTypeParser(), context); + } + + @Override + public SchemaRefresh parse() { + ImmutableMap.Builder keyspacesBuilder = ImmutableMap.builder(); + for (AdminRow row : rows.keyspaces()) { + KeyspaceMetadata keyspace = parseKeyspace(row); + keyspacesBuilder.put(keyspace.getName(), keyspace); + } + for (AdminRow row : rows.virtualKeyspaces()) { + KeyspaceMetadata keyspace = parseVirtualKeyspace(row); + keyspacesBuilder.put(keyspace.getName(), keyspace); + } + SchemaRefresh refresh = new SchemaRefresh(keyspacesBuilder.build()); + LOG.debug("[{}] Schema parsing took {}", logPrefix, NanoTime.formatTimeSince(startTimeNs)); + return refresh; + } + + private KeyspaceMetadata parseKeyspace(AdminRow keyspaceRow) { + + // Cassandra <= 2.2 + // CREATE TABLE system.schema_keyspaces ( + // keyspace_name text PRIMARY KEY, + // durable_writes boolean, + // strategy_class text, + // strategy_options text + // ) + // + // Cassandra >= 3.0: + // CREATE TABLE system_schema.keyspaces ( + // keyspace_name text PRIMARY KEY, + // durable_writes boolean, + // replication frozen> + // ) + CqlIdentifier keyspaceId = CqlIdentifier.fromInternal(keyspaceRow.getString("keyspace_name")); + boolean durableWrites = + MoreObjects.firstNonNull(keyspaceRow.getBoolean("durable_writes"), false); + + Map replicationOptions; + if (keyspaceRow.contains("strategy_class")) { + String strategyClass = keyspaceRow.getString("strategy_class"); + Map strategyOptions = + SimpleJsonParser.parseStringMap(keyspaceRow.getString("strategy_options")); + replicationOptions = + ImmutableMap.builder() + .putAll(strategyOptions) + .put("class", strategyClass) + .build(); + } else { + replicationOptions = keyspaceRow.getMapOfStringToString("replication"); + } + + Map types = parseTypes(keyspaceId); + + return new DefaultKeyspaceMetadata( + keyspaceId, + durableWrites, + false, + replicationOptions, + types, + parseTables(keyspaceId, types), + parseViews(keyspaceId, types), + parseFunctions(keyspaceId, types), + parseAggregates(keyspaceId, types)); + } + + private KeyspaceMetadata parseVirtualKeyspace(AdminRow keyspaceRow) { + + CqlIdentifier keyspaceId = CqlIdentifier.fromInternal(keyspaceRow.getString("keyspace_name")); + boolean durableWrites = + MoreObjects.firstNonNull(keyspaceRow.getBoolean("durable_writes"), false); + + Map replicationOptions = Collections.emptyMap(); + ; + + Map types = parseTypes(keyspaceId); + + return new DefaultKeyspaceMetadata( + keyspaceId, + durableWrites, + true, + replicationOptions, + types, + parseVirtualTables(keyspaceId, types), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap()); + } + + private Map parseTypes(CqlIdentifier keyspaceId) { + return userDefinedTypeParser.parse(rows.types().get(keyspaceId), keyspaceId); + } + + private Map parseVirtualTables( + CqlIdentifier keyspaceId, Map types) { + ImmutableMap.Builder tablesBuilder = ImmutableMap.builder(); + for (AdminRow tableRow : rows.virtualTables().get(keyspaceId)) { + TableMetadata table = tableParser.parseVirtualTable(tableRow, keyspaceId, types); + if (table != null) { + tablesBuilder.put(table.getName(), table); + } + } + return tablesBuilder.build(); + } + + private Map parseTables( + CqlIdentifier keyspaceId, Map types) { + ImmutableMap.Builder tablesBuilder = ImmutableMap.builder(); + for (AdminRow tableRow : rows.tables().get(keyspaceId)) { + TableMetadata table = tableParser.parseTable(tableRow, keyspaceId, types); + if (table != null) { + tablesBuilder.put(table.getName(), table); + } + } + return tablesBuilder.build(); + } + + private Map parseViews( + CqlIdentifier keyspaceId, Map types) { + ImmutableMap.Builder viewsBuilder = ImmutableMap.builder(); + for (AdminRow viewRow : rows.views().get(keyspaceId)) { + ViewMetadata view = viewParser.parseView(viewRow, keyspaceId, types); + if (view != null) { + viewsBuilder.put(view.getName(), view); + } + } + return viewsBuilder.build(); + } + + private Map parseFunctions( + CqlIdentifier keyspaceId, Map types) { + ImmutableMap.Builder functionsBuilder = + ImmutableMap.builder(); + for (AdminRow functionRow : rows.functions().get(keyspaceId)) { + FunctionMetadata function = functionParser.parseFunction(functionRow, keyspaceId, types); + if (function != null) { + functionsBuilder.put(function.getSignature(), function); + } + } + return functionsBuilder.build(); + } + + private Map parseAggregates( + CqlIdentifier keyspaceId, Map types) { + ImmutableMap.Builder aggregatesBuilder = + ImmutableMap.builder(); + for (AdminRow aggregateRow : rows.aggregates().get(keyspaceId)) { + AggregateMetadata aggregate = aggregateParser.parseAggregate(aggregateRow, keyspaceId, types); + if (aggregate != null) { + aggregatesBuilder.put(aggregate.getSignature(), aggregate); + } + } + return aggregatesBuilder.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeClassNameCompositeParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeClassNameCompositeParser.java new file mode 100644 index 00000000000..225004ca9ab --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeClassNameCompositeParser.java @@ -0,0 +1,101 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DataTypeClassNameCompositeParser extends DataTypeClassNameParser { + + public ParseResult parseWithComposite( + String className, + CqlIdentifier keyspaceId, + Map userTypes, + InternalDriverContext context) { + Parser parser = new Parser(className, 0); + + String next = parser.parseNextName(); + if (!isComposite(next)) { + return new ParseResult(parse(keyspaceId, className, userTypes, context), isReversed(next)); + } + + List subClassNames = parser.getTypeParameters(); + int count = subClassNames.size(); + String last = subClassNames.get(count - 1); + Map collections = new HashMap<>(); + if (isCollection(last)) { + count--; + Parser collectionParser = new Parser(last, 0); + collectionParser.parseNextName(); // skips columnToCollectionType + Map params = collectionParser.getCollectionsParameters(); + for (Map.Entry entry : params.entrySet()) { + collections.put(entry.getKey(), parse(keyspaceId, entry.getValue(), userTypes, context)); + } + } + + List types = new ArrayList<>(count); + List reversed = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + types.add(parse(keyspaceId, subClassNames.get(i), userTypes, context)); + reversed.add(isReversed(subClassNames.get(i))); + } + + return new ParseResult(true, types, reversed, collections); + } + + public static class ParseResult { + public final boolean isComposite; + public final List types; + public final List reversed; + public final Map collections; + + private ParseResult(DataType type, boolean reversed) { + this( + false, + Collections.singletonList(type), + Collections.singletonList(reversed), + Collections.emptyMap()); + } + + private ParseResult( + boolean isComposite, + List types, + List reversed, + Map collections) { + this.isComposite = isComposite; + this.types = types; + this.reversed = reversed; + this.collections = collections; + } + } + + private static boolean isComposite(String className) { + return className.startsWith("org.apache.cassandra.db.marshal.CompositeType"); + } + + private static boolean isCollection(String className) { + return className.startsWith("org.apache.cassandra.db.marshal.ColumnToCollectionType"); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeClassNameParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeClassNameParser.java new file mode 100644 index 00000000000..d2470c0d48a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeClassNameParser.java @@ -0,0 +1,374 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.type.DefaultTupleType; +import com.datastax.oss.driver.internal.core.type.UserDefinedTypeBuilder; +import com.datastax.oss.driver.internal.core.type.codec.ParseUtils; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Parses data types from schema tables, for Cassandra 2.2 and below. + * + *

In these versions, data types appear as class names, like + * "org.apache.cassandra.db.marshal.AsciiType" or + * "org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.Int32Type)". + * + *

This is modified (and simplified) from Cassandra's {@code TypeParser} class to suit our needs. + * In particular it's not very efficient, but it doesn't really matter since it's rarely used and + * never in a critical path. + */ +@ThreadSafe +public class DataTypeClassNameParser implements DataTypeParser { + + private static final Logger LOG = LoggerFactory.getLogger(DataTypeClassNameParser.class); + + @Override + public DataType parse( + CqlIdentifier keyspaceId, + String toParse, + Map userTypes, + InternalDriverContext context) { + boolean frozen = false; + if (isReversed(toParse)) { + // Just skip the ReversedType part, we don't care + toParse = getNestedClassName(toParse); + } else if (toParse.startsWith("org.apache.cassandra.db.marshal.FrozenType")) { + frozen = true; + toParse = getNestedClassName(toParse); + } + + Parser parser = new Parser(toParse, 0); + String next = parser.parseNextName(); + + if (next.startsWith("org.apache.cassandra.db.marshal.ListType")) { + DataType elementType = + parse(keyspaceId, parser.getTypeParameters().get(0), userTypes, context); + return DataTypes.listOf(elementType, frozen); + } + + if (next.startsWith("org.apache.cassandra.db.marshal.SetType")) { + DataType elementType = + parse(keyspaceId, parser.getTypeParameters().get(0), userTypes, context); + return DataTypes.setOf(elementType, frozen); + } + + if (next.startsWith("org.apache.cassandra.db.marshal.MapType")) { + List parameters = parser.getTypeParameters(); + DataType keyType = parse(keyspaceId, parameters.get(0), userTypes, context); + DataType valueType = parse(keyspaceId, parameters.get(1), userTypes, context); + return DataTypes.mapOf(keyType, valueType, frozen); + } + + if (frozen) + LOG.warn( + "[{}] Got o.a.c.db.marshal.FrozenType for something else than a collection, " + + "this driver version might be too old for your version of Cassandra", + context.getSessionName()); + + if (next.startsWith("org.apache.cassandra.db.marshal.UserType")) { + ++parser.idx; // skipping '(' + + CqlIdentifier keyspace = CqlIdentifier.fromInternal(parser.readOne()); + parser.skipBlankAndComma(); + String typeName = + TypeCodecs.TEXT.decode( + Bytes.fromHexString("0x" + parser.readOne()), context.getProtocolVersion()); + if (typeName == null) { + throw new AssertionError("Type name cannot be null, this is a server bug"); + } + CqlIdentifier typeId = CqlIdentifier.fromInternal(typeName); + Map nameAndTypeParameters = parser.getNameAndTypeParameters(); + + // Avoid re-parsing if we already have the definition + if (userTypes != null && userTypes.containsKey(typeId)) { + // copy as frozen since C* 2.x UDTs are always frozen. + return userTypes.get(typeId).copy(true); + } else { + UserDefinedTypeBuilder builder = new UserDefinedTypeBuilder(keyspace, typeId); + parser.skipBlankAndComma(); + for (Map.Entry entry : nameAndTypeParameters.entrySet()) { + CqlIdentifier fieldName = CqlIdentifier.fromInternal(entry.getKey()); + DataType fieldType = parse(keyspaceId, entry.getValue(), userTypes, context); + builder.withField(fieldName, fieldType); + } + // create a frozen UserType since C* 2.x UDTs are always frozen. + return builder.frozen().build(); + } + } + + if (next.startsWith("org.apache.cassandra.db.marshal.TupleType")) { + List rawTypes = parser.getTypeParameters(); + ImmutableList.Builder componentTypesBuilder = ImmutableList.builder(); + for (String rawType : rawTypes) { + componentTypesBuilder.add(parse(keyspaceId, rawType, userTypes, context)); + } + return new DefaultTupleType(componentTypesBuilder.build(), context); + } + + DataType type = NATIVE_TYPES_BY_CLASS_NAME.get(next); + return type == null ? DataTypes.custom(toParse) : type; + } + + static boolean isReversed(String toParse) { + return toParse.startsWith("org.apache.cassandra.db.marshal.ReversedType"); + } + + private static String getNestedClassName(String className) { + Parser p = new Parser(className, 0); + p.parseNextName(); + List l = p.getTypeParameters(); + if (l.size() != 1) { + throw new IllegalStateException(); + } + className = l.get(0); + return className; + } + + static class Parser { + + private final String str; + private int idx; + + Parser(String str, int idx) { + this.str = str; + this.idx = idx; + } + + String parseNextName() { + skipBlank(); + return readNextIdentifier(); + } + + private String readOne() { + String name = parseNextName(); + String args = readRawArguments(); + return name + args; + } + + // Assumes we have just read a class name and read it's potential arguments + // blindly. I.e. it assume that either parsing is done or that we're on a '(' + // and this reads everything up until the corresponding closing ')'. It + // returns everything read, including the enclosing parenthesis. + private String readRawArguments() { + skipBlank(); + + if (isEOS() || str.charAt(idx) == ')' || str.charAt(idx) == ',') { + return ""; + } + + if (str.charAt(idx) != '(') { + throw new IllegalStateException( + String.format( + "Expecting char %d of %s to be '(' but '%c' found", idx, str, str.charAt(idx))); + } + + int i = idx; + int open = 1; + while (open > 0) { + ++idx; + + if (isEOS()) { + throw new IllegalStateException("Non closed parenthesis"); + } + + if (str.charAt(idx) == '(') { + open++; + } else if (str.charAt(idx) == ')') { + open--; + } + } + // we've stopped at the last closing ')' so move past that + ++idx; + return str.substring(i, idx); + } + + List getTypeParameters() { + List list = new ArrayList<>(); + + if (isEOS()) { + return list; + } + + if (str.charAt(idx) != '(') { + throw new IllegalStateException(); + } + + ++idx; // skipping '(' + + while (skipBlankAndComma()) { + if (str.charAt(idx) == ')') { + ++idx; + return list; + } + list.add(readOne()); + } + throw new IllegalArgumentException( + String.format( + "Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); + } + + Map getCollectionsParameters() { + if (isEOS()) { + return Collections.emptyMap(); + } + if (str.charAt(idx) != '(') { + throw new IllegalStateException(); + } + ++idx; // skipping '(' + return getNameAndTypeParameters(); + } + + // Must be at the start of the first parameter to read + private Map getNameAndTypeParameters() { + // The order of the hashmap matters for UDT + Map map = new LinkedHashMap<>(); + + while (skipBlankAndComma()) { + if (str.charAt(idx) == ')') { + ++idx; + return map; + } + + String bbHex = readNextIdentifier(); + String name = null; + try { + name = + TypeCodecs.TEXT.decode( + Bytes.fromHexString("0x" + bbHex), DefaultProtocolVersion.DEFAULT); + } catch (NumberFormatException e) { + throwSyntaxError(e.getMessage()); + } + + skipBlank(); + if (str.charAt(idx) != ':') { + throwSyntaxError("expecting ':' token"); + } + + ++idx; + skipBlank(); + map.put(name, readOne()); + } + throw new IllegalArgumentException( + String.format( + "Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); + } + + private void throwSyntaxError(String msg) { + throw new IllegalArgumentException( + String.format("Syntax error parsing '%s' at char %d: %s", str, idx, msg)); + } + + private boolean isEOS() { + return isEOS(str, idx); + } + + private static boolean isEOS(String str, int i) { + return i >= str.length(); + } + + private void skipBlank() { + idx = skipBlank(str, idx); + } + + private static int skipBlank(String str, int i) { + while (!isEOS(str, i) && ParseUtils.isBlank(str.charAt(i))) { + ++i; + } + return i; + } + + // skip all blank and at best one comma, return true if there not EOS + private boolean skipBlankAndComma() { + boolean commaFound = false; + while (!isEOS()) { + int c = str.charAt(idx); + if (c == ',') { + if (commaFound) { + return true; + } else { + commaFound = true; + } + } else if (!ParseUtils.isBlank(c)) { + return true; + } + ++idx; + } + return false; + } + + // left idx positioned on the character stopping the read + private String readNextIdentifier() { + int i = idx; + while (!isEOS() && ParseUtils.isCqlIdentifierChar(str.charAt(idx))) { + ++idx; + } + return str.substring(i, idx); + } + + @Override + public String toString() { + return str.substring(0, idx) + + "[" + + (idx == str.length() ? "" : str.charAt(idx)) + + "]" + + str.substring(idx + 1); + } + } + + @VisibleForTesting + static ImmutableMap NATIVE_TYPES_BY_CLASS_NAME = + new ImmutableMap.Builder() + .put("org.apache.cassandra.db.marshal.AsciiType", DataTypes.ASCII) + .put("org.apache.cassandra.db.marshal.LongType", DataTypes.BIGINT) + .put("org.apache.cassandra.db.marshal.BytesType", DataTypes.BLOB) + .put("org.apache.cassandra.db.marshal.BooleanType", DataTypes.BOOLEAN) + .put("org.apache.cassandra.db.marshal.CounterColumnType", DataTypes.COUNTER) + .put("org.apache.cassandra.db.marshal.DecimalType", DataTypes.DECIMAL) + .put("org.apache.cassandra.db.marshal.DoubleType", DataTypes.DOUBLE) + .put("org.apache.cassandra.db.marshal.FloatType", DataTypes.FLOAT) + .put("org.apache.cassandra.db.marshal.InetAddressType", DataTypes.INET) + .put("org.apache.cassandra.db.marshal.Int32Type", DataTypes.INT) + .put("org.apache.cassandra.db.marshal.UTF8Type", DataTypes.TEXT) + .put("org.apache.cassandra.db.marshal.TimestampType", DataTypes.TIMESTAMP) + .put("org.apache.cassandra.db.marshal.SimpleDateType", DataTypes.DATE) + .put("org.apache.cassandra.db.marshal.TimeType", DataTypes.TIME) + .put("org.apache.cassandra.db.marshal.UUIDType", DataTypes.UUID) + .put("org.apache.cassandra.db.marshal.IntegerType", DataTypes.VARINT) + .put("org.apache.cassandra.db.marshal.TimeUUIDType", DataTypes.TIMEUUID) + .put("org.apache.cassandra.db.marshal.ByteType", DataTypes.TINYINT) + .put("org.apache.cassandra.db.marshal.ShortType", DataTypes.SMALLINT) + .put("org.apache.cassandra.db.marshal.DurationType", DataTypes.DURATION) + .build(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeCqlNameParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeCqlNameParser.java new file mode 100644 index 00000000000..0ab6999f91b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeCqlNameParser.java @@ -0,0 +1,318 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.schema.ShallowUserDefinedType; +import com.datastax.oss.driver.internal.core.type.DefaultTupleType; +import com.datastax.oss.driver.internal.core.type.codec.ParseUtils; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; + +/** + * Parses data types from schema tables, for Cassandra 3.0 and above. + * + *

In these versions, data types appear as string literals, like "ascii" or + * "tuple<int,int>". + */ +@ThreadSafe +public class DataTypeCqlNameParser implements DataTypeParser { + + @Override + public DataType parse( + CqlIdentifier keyspaceId, + String toParse, + Map userTypes, + InternalDriverContext context) { + // Top-level is never frozen, it is only set recursively when we encounter the frozen<> keyword + return parse(toParse, keyspaceId, false, userTypes, context); + } + + private DataType parse( + String toParse, + CqlIdentifier keyspaceId, + boolean frozen, + Map userTypes, + InternalDriverContext context) { + + if (toParse.startsWith("'")) { + return DataTypes.custom(toParse.substring(1, toParse.length() - 1)); + } + + Parser parser = new Parser(toParse, 0); + String type = parser.parseTypeName(); + + DataType nativeType = NATIVE_TYPES_BY_NAME.get(type.toLowerCase()); + if (nativeType != null) { + return nativeType; + } + + if (type.equalsIgnoreCase("list")) { + List parameters = parser.parseTypeParameters(); + if (parameters.size() != 1) { + throw new IllegalArgumentException( + String.format("Expecting single parameter for list, got %s", parameters)); + } + DataType elementType = parse(parameters.get(0), keyspaceId, false, userTypes, context); + return DataTypes.listOf(elementType, frozen); + } + + if (type.equalsIgnoreCase("set")) { + List parameters = parser.parseTypeParameters(); + if (parameters.size() != 1) { + throw new IllegalArgumentException( + String.format("Expecting single parameter for set, got %s", parameters)); + } + DataType elementType = parse(parameters.get(0), keyspaceId, false, userTypes, context); + return DataTypes.setOf(elementType, frozen); + } + + if (type.equalsIgnoreCase("map")) { + List parameters = parser.parseTypeParameters(); + if (parameters.size() != 2) { + throw new IllegalArgumentException( + String.format("Expecting two parameters for map, got %s", parameters)); + } + DataType keyType = parse(parameters.get(0), keyspaceId, false, userTypes, context); + DataType valueType = parse(parameters.get(1), keyspaceId, false, userTypes, context); + return DataTypes.mapOf(keyType, valueType, frozen); + } + + if (type.equalsIgnoreCase("frozen")) { + List parameters = parser.parseTypeParameters(); + if (parameters.size() != 1) { + throw new IllegalArgumentException( + String.format("Expecting single parameter for frozen keyword, got %s", parameters)); + } + return parse(parameters.get(0), keyspaceId, true, userTypes, context); + } + + if (type.equalsIgnoreCase("tuple")) { + List rawTypes = parser.parseTypeParameters(); + ImmutableList.Builder componentTypesBuilder = ImmutableList.builder(); + for (String rawType : rawTypes) { + componentTypesBuilder.add(parse(rawType, keyspaceId, false, userTypes, context)); + } + return new DefaultTupleType(componentTypesBuilder.build(), context); + } + + // Otherwise it's a UDT + CqlIdentifier name = CqlIdentifier.fromCql(type); + if (userTypes != null) { + UserDefinedType userType = userTypes.get(name); + if (userType == null) { + throw new IllegalStateException(String.format("Can't find referenced user type %s", type)); + } + return userType.copy(frozen); + } else { + return new ShallowUserDefinedType(keyspaceId, name, frozen); + } + } + + private static class Parser { + + private final String str; + + private int idx; + + Parser(String str, int idx) { + this.str = str; + this.idx = idx; + } + + String parseTypeName() { + idx = ParseUtils.skipSpaces(str, idx); + return readNextIdentifier(); + } + + List parseTypeParameters() { + List list = new ArrayList<>(); + + if (isEOS()) { + return list; + } + + skipBlankAndComma(); + + if (str.charAt(idx) != '<') { + throw new IllegalStateException(); + } + + ++idx; // skipping '<' + + while (skipBlankAndComma()) { + if (str.charAt(idx) == '>') { + ++idx; + return list; + } + + String name = parseTypeName(); + String args = readRawTypeParameters(); + list.add(name + args); + } + throw new IllegalArgumentException( + String.format( + "Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); + } + + // left idx positioned on the character stopping the read + private String readNextIdentifier() { + int startIdx = idx; + if (str.charAt(startIdx) == '"') { // case-sensitive name included in double quotes + ++idx; + // read until closing quote. + while (!isEOS()) { + boolean atQuote = str.charAt(idx) == '"'; + ++idx; + if (atQuote) { + // if the next character is also a quote, this is an escaped quote, continue reading, + // otherwise stop. + if (!isEOS() && str.charAt(idx) == '"') { + ++idx; + } else { + break; + } + } + } + } else if (str.charAt(startIdx) == '\'') { // custom type name included in single quotes + ++idx; + // read until closing quote. + while (!isEOS() && str.charAt(idx++) != '\'') { + /* loop */ + } + } else { + while (!isEOS() + && (ParseUtils.isCqlIdentifierChar(str.charAt(idx)) || str.charAt(idx) == '"')) { + ++idx; + } + } + return str.substring(startIdx, idx); + } + + // Assumes we have just read a type name and read its potential arguments blindly. I.e. it + // assumes that either parsing is done or that we're on a '<' and this reads everything up until + // the corresponding closing '>'. It returns everything read, including the enclosing brackets. + private String readRawTypeParameters() { + idx = ParseUtils.skipSpaces(str, idx); + + if (isEOS() || str.charAt(idx) == '>' || str.charAt(idx) == ',') { + return ""; + } + + if (str.charAt(idx) != '<') { + throw new IllegalStateException( + String.format( + "Expecting char %d of %s to be '<' but '%c' found", idx, str, str.charAt(idx))); + } + + int i = idx; + int open = 1; + boolean inQuotes = false; + while (open > 0) { + ++idx; + + if (isEOS()) { + throw new IllegalStateException("Non closed angle brackets"); + } + + // Only parse for '<' and '>' characters if not within a quoted identifier. + // Note we don't need to handle escaped quotes ("") in type names here, because they just + // cause inQuotes to flip to false and immediately back to true + if (!inQuotes) { + if (str.charAt(idx) == '"') { + inQuotes = true; + } else if (str.charAt(idx) == '<') { + open++; + } else if (str.charAt(idx) == '>') { + open--; + } + } else if (str.charAt(idx) == '"') { + inQuotes = false; + } + } + // we've stopped at the last closing ')' so move past that + ++idx; + return str.substring(i, idx); + } + + // skip all blank and at best one comma, return true if there not EOS + private boolean skipBlankAndComma() { + boolean commaFound = false; + while (!isEOS()) { + int c = str.charAt(idx); + if (c == ',') { + if (commaFound) { + return true; + } else { + commaFound = true; + } + } else if (!ParseUtils.isBlank(c)) { + return true; + } + ++idx; + } + return false; + } + + private boolean isEOS() { + return idx >= str.length(); + } + + @Override + public String toString() { + return str.substring(0, idx) + + "[" + + (idx == str.length() ? "" : str.charAt(idx)) + + "]" + + str.substring(idx + 1); + } + } + + @VisibleForTesting + static final ImmutableMap NATIVE_TYPES_BY_NAME = + new ImmutableMap.Builder() + .put("ascii", DataTypes.ASCII) + .put("bigint", DataTypes.BIGINT) + .put("blob", DataTypes.BLOB) + .put("boolean", DataTypes.BOOLEAN) + .put("counter", DataTypes.COUNTER) + .put("decimal", DataTypes.DECIMAL) + .put("double", DataTypes.DOUBLE) + .put("float", DataTypes.FLOAT) + .put("inet", DataTypes.INET) + .put("int", DataTypes.INT) + .put("text", DataTypes.TEXT) + .put("varchar", DataTypes.TEXT) + .put("timestamp", DataTypes.TIMESTAMP) + .put("date", DataTypes.DATE) + .put("time", DataTypes.TIME) + .put("uuid", DataTypes.UUID) + .put("varint", DataTypes.VARINT) + .put("timeuuid", DataTypes.TIMEUUID) + .put("tinyint", DataTypes.TINYINT) + .put("smallint", DataTypes.SMALLINT) + .put("duration", DataTypes.DURATION) + .build(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeParser.java new file mode 100644 index 00000000000..42ee4c37b05 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeParser.java @@ -0,0 +1,61 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.schema.ShallowUserDefinedType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** Parses data types from their string representation in schema tables. */ +public interface DataTypeParser { + + /** + * @param userTypes the UDTs in the current keyspace, if we know them already. This is used to + * resolve subtypes if the type to parse is complex (such as {@code list}). The only + * situation where we don't have them is when we refresh all the UDTs of a keyspace; in that + * case, the filed will be {@code null} and any UDT encountered by this method will always be + * re-created from scratch: for Cassandra < 2.2, this means parsing the whole definition; + * for > 3.0, this means materializing it as a {@link ShallowUserDefinedType} that will be + * resolved in a second pass. + */ + DataType parse( + CqlIdentifier keyspaceId, + String toParse, + Map userTypes, + InternalDriverContext context); + + default List parse( + CqlIdentifier keyspaceId, + List typeStrings, + Map userTypes, + InternalDriverContext context) { + if (typeStrings.isEmpty()) { + return Collections.emptyList(); + } else { + ImmutableList.Builder builder = ImmutableList.builder(); + for (String typeString : typeStrings) { + builder.add(parse(keyspaceId, typeString, userTypes, context)); + } + return builder.build(); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DefaultSchemaParserFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DefaultSchemaParserFactory.java new file mode 100644 index 00000000000..9a4a5bf148a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DefaultSchemaParserFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DefaultSchemaParserFactory implements SchemaParserFactory { + + private final InternalDriverContext context; + + public DefaultSchemaParserFactory(InternalDriverContext context) { + this.context = context; + } + + @Override + public SchemaParser newInstance(SchemaRows rows) { + return new CassandraSchemaParser(rows, context); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/FunctionParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/FunctionParser.java new file mode 100644 index 00000000000..edd5a6bfe8f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/FunctionParser.java @@ -0,0 +1,108 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.schema.DefaultFunctionMetadata; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class FunctionParser { + + private static final Logger LOG = LoggerFactory.getLogger(FunctionParser.class); + + private final DataTypeParser dataTypeParser; + private final InternalDriverContext context; + private final String logPrefix; + + public FunctionParser(DataTypeParser dataTypeParser, InternalDriverContext context) { + this.dataTypeParser = dataTypeParser; + this.context = context; + this.logPrefix = context.getSessionName(); + } + + public FunctionMetadata parseFunction( + AdminRow row, + CqlIdentifier keyspaceId, + Map userDefinedTypes) { + // Cassandra < 3.0: + // CREATE TABLE system.schema_functions ( + // keyspace_name text, + // function_name text, + // signature frozen>, + // argument_names list, + // argument_types list, + // body text, + // called_on_null_input boolean, + // language text, + // return_type text, + // PRIMARY KEY (keyspace_name, function_name, signature) + // ) WITH CLUSTERING ORDER BY (function_name ASC, signature ASC) + // + // Cassandra >= 3.0: + // CREATE TABLE system_schema.functions ( + // keyspace_name text, + // function_name text, + // argument_names frozen>, + // argument_types frozen>, + // body text, + // called_on_null_input boolean, + // language text, + // return_type text, + // PRIMARY KEY (keyspace_name, function_name, argument_types) + // ) WITH CLUSTERING ORDER BY (function_name ASC, argument_types ASC) + String simpleName = row.getString("function_name"); + List argumentNames = + ImmutableList.copyOf( + Lists.transform(row.getListOfString("argument_names"), CqlIdentifier::fromInternal)); + List argumentTypes = row.getListOfString("argument_types"); + if (argumentNames.size() != argumentTypes.size()) { + LOG.warn( + "[{}] Error parsing system row for function {}.{}, " + + "number of argument names and types don't match (got {} and {}).", + logPrefix, + keyspaceId.asInternal(), + simpleName, + argumentNames.size(), + argumentTypes.size()); + return null; + } + FunctionSignature signature = + new FunctionSignature( + CqlIdentifier.fromInternal(simpleName), + dataTypeParser.parse(keyspaceId, argumentTypes, userDefinedTypes, context)); + String body = row.getString("body"); + Boolean calledOnNullInput = row.getBoolean("called_on_null_input"); + String language = row.getString("language"); + DataType returnType = + dataTypeParser.parse(keyspaceId, row.getString("return_type"), userDefinedTypes, context); + + return new DefaultFunctionMetadata( + keyspaceId, signature, argumentNames, body, calledOnNullInput, language, returnType); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/RawColumn.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/RawColumn.java new file mode 100644 index 00000000000..c9f65de601d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/RawColumn.java @@ -0,0 +1,215 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.driver.shaded.guava.common.primitives.Ints; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import net.jcip.annotations.NotThreadSafe; + +/** + * An intermediary format to manipulate columns before we turn them into {@link ColumnMetadata} + * instances. + */ +@NotThreadSafe +public class RawColumn implements Comparable { + + public static final String KIND_PARTITION_KEY = "partition_key"; + public static final String KIND_CLUSTERING_COLUMN = "clustering"; + public static final String KIND_REGULAR = "regular"; + public static final String KIND_COMPACT_VALUE = "compact_value"; + public static final String KIND_STATIC = "static"; + + public final CqlIdentifier name; + public String kind; + public final int position; + public final String dataType; + public final boolean reversed; + public final String indexName; + public final String indexType; + public final Map indexOptions; + + private RawColumn( + AdminRow row, CqlIdentifier keyspaceId, Map userTypes) { + // Cassandra < 3.0: + // CREATE TABLE system.schema_columns ( + // keyspace_name text, + // columnfamily_name text, + // column_name text, + // component_index int, + // index_name text, + // index_options text, + // index_type text, + // type text, + // validator text, + // PRIMARY KEY (keyspace_name, columnfamily_name, column_name) + // ) WITH CLUSTERING ORDER BY (columnfamily_name ASC, column_name ASC) + // + // Cassandra >= 3.0: + // CREATE TABLE system_schema.columns ( + // keyspace_name text, + // table_name text, + // column_name text, + // clustering_order text, + // column_name_bytes blob, + // kind text, + // position int, + // type text, + // PRIMARY KEY (keyspace_name, table_name, column_name) + // ) WITH CLUSTERING ORDER BY (table_name ASC, column_name ASC) + this.name = CqlIdentifier.fromInternal(row.getString("column_name")); + if (row.contains("kind")) { + this.kind = row.getString("kind"); + } else { + this.kind = row.getString("type"); + // remap clustering_key to KIND_CLUSTERING_COLUMN so code doesn't have to check for both. + if (this.kind.equals("clustering_key")) { + this.kind = KIND_CLUSTERING_COLUMN; + } + } + + Integer rawPosition = + row.contains("position") ? row.getInteger("position") : row.getInteger("component_index"); + this.position = (rawPosition == null || rawPosition == -1) ? 0 : rawPosition; + + this.dataType = row.contains("validator") ? row.getString("validator") : row.getString("type"); + this.reversed = + row.contains("clustering_order") + ? "desc".equals(row.getString("clustering_order")) + : DataTypeClassNameParser.isReversed(dataType); + this.indexName = row.getString("index_name"); + this.indexType = row.getString("index_type"); + // index_options can apparently contain the string 'null' (JAVA-834) + String indexOptionsString = row.getString("index_options"); + this.indexOptions = + (indexOptionsString == null || indexOptionsString.equals("null")) + ? Collections.emptyMap() + : SimpleJsonParser.parseStringMap(indexOptionsString); + } + + @Override + public int compareTo(@NonNull RawColumn that) { + // First, order by kind. Then order partition key and clustering columns by position. For + // other kinds, order by column name. + if (!this.kind.equals(that.kind)) { + return Ints.compare(rank(this.kind), rank(that.kind)); + } else if (kind.equals(KIND_PARTITION_KEY) || kind.equals(KIND_CLUSTERING_COLUMN)) { + return Integer.compare(this.position, that.position); + } else { + return this.name.asInternal().compareTo(that.name.asInternal()); + } + } + + private static int rank(String kind) { + switch (kind) { + case KIND_PARTITION_KEY: + return 1; + case KIND_CLUSTERING_COLUMN: + return 2; + case KIND_REGULAR: + return 3; + case KIND_COMPACT_VALUE: + return 4; + case KIND_STATIC: + return 5; + default: + return Integer.MAX_VALUE; + } + } + + public static List toRawColumns( + Collection rows, + CqlIdentifier keyspaceId, + Map userTypes) { + if (rows.isEmpty()) { + return Collections.emptyList(); + } else { + // Use a mutable list, we might remove some elements later + List result = Lists.newArrayListWithExpectedSize(rows.size()); + for (AdminRow row : rows) { + result.add(new RawColumn(row, keyspaceId, userTypes)); + } + return result; + } + } + + /** + * Helper method to filter columns while parsing a table's metadata. + * + *

Upon migration from thrift to CQL, we internally create a pair of surrogate + * clustering/regular columns for compact static tables. These columns shouldn't be exposed to the + * user but are currently returned by C*. We also need to remove the static keyword for all other + * columns in the table. + */ + public static void pruneStaticCompactTableColumns(List columns) { + ListIterator iterator = columns.listIterator(); + while (iterator.hasNext()) { + RawColumn column = iterator.next(); + switch (column.kind) { + case KIND_CLUSTERING_COLUMN: + case KIND_REGULAR: + iterator.remove(); + break; + case KIND_STATIC: + column.kind = KIND_REGULAR; + break; + default: + // nothing to do + } + } + } + + /** + * Helper method to filter columns while parsing a table's metadata. + * + *

Upon migration from thrift to CQL, we internally create a surrogate column "value" of type + * EmptyType for dense tables. This column shouldn't be exposed to the user but is currently + * returned by C*. + */ + public static void pruneDenseTableColumnsV3(List columns) { + ListIterator iterator = columns.listIterator(); + while (iterator.hasNext()) { + RawColumn column = iterator.next(); + if (column.kind.equals(KIND_REGULAR) && "empty".equals(column.dataType)) { + iterator.remove(); + } + } + } + + /** + * Helper method to filter columns while parsing a table's metadata. + * + *

This is similar to {@link #pruneDenseTableColumnsV3(List)}, but for legacy C* versions. + */ + public static void pruneDenseTableColumnsV2(List columns) { + ListIterator iterator = columns.listIterator(); + while (iterator.hasNext()) { + RawColumn column = iterator.next(); + if (column.kind.equals(KIND_COMPACT_VALUE) && column.name.asInternal().isEmpty()) { + iterator.remove(); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/RelationParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/RelationParser.java new file mode 100644 index 00000000000..43b942b1669 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/RelationParser.java @@ -0,0 +1,144 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.RelationMetadata; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.schema.ScriptBuilder; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.nio.ByteBuffer; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; + +// Shared code for table and view parsing +@ThreadSafe +public abstract class RelationParser { + + protected final SchemaRows rows; + protected final InternalDriverContext context; + protected final String logPrefix; + + protected RelationParser(SchemaRows rows, InternalDriverContext context) { + this.rows = rows; + this.context = context; + this.logPrefix = context.getSessionName(); + } + + protected Map parseOptions(AdminRow row) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry> entry : OPTION_CODECS.entrySet()) { + String name = entry.getKey(); + CqlIdentifier id = CqlIdentifier.fromInternal(name); + TypeCodec codec = entry.getValue(); + + if (name.equals("caching") && row.isString("caching")) { + // C* <=2.2, caching is stored as a string, and also appears as a string in the WITH clause. + builder.put(id, row.getString(name)); + } else if (name.equals("compaction_strategy_class")) { + // C* <=2.2, compaction options split in two columns + String strategyClass = row.getString(name); + if (strategyClass != null) { + builder.put( + CqlIdentifier.fromInternal("compaction"), + ImmutableMap.builder() + .put("class", strategyClass) + .putAll( + SimpleJsonParser.parseStringMap(row.getString("compaction_strategy_options"))) + .build()); + } + } else if (name.equals("compression_parameters")) { + // C* <=2.2, compression stored as a string + String compressionParameters = row.getString(name); + if (compressionParameters != null) { + builder.put( + CqlIdentifier.fromInternal("compression"), + ImmutableMap.copyOf(SimpleJsonParser.parseStringMap(row.getString(name)))); + } + } else { + // Default case, read the value in a generic fashion + Object value = row.get(name, codec); + if (value != null) { + builder.put(id, value); + } + } + } + return builder.build(); + } + + public static void appendOptions(Map options, ScriptBuilder builder) { + for (Map.Entry entry : options.entrySet()) { + CqlIdentifier name = entry.getKey(); + Object value = entry.getValue(); + String formattedValue; + if (name.asInternal().equals("caching") && value instanceof String) { + formattedValue = TypeCodecs.TEXT.format((String) value); + } else { + @SuppressWarnings("unchecked") + TypeCodec codec = + (TypeCodec) RelationParser.OPTION_CODECS.get(name.asInternal()); + formattedValue = codec.format(value); + } + String optionName = name.asCql(true); + if ("local_read_repair_chance".equals(optionName)) { + // Another small quirk in C* <= 2.2 + optionName = "dclocal_read_repair_chance"; + } + builder.andWith().append(optionName).append(" = ").append(formattedValue); + } + } + + public static final TypeCodec> MAP_OF_TEXT_TO_TEXT = + TypeCodecs.mapOf(TypeCodecs.TEXT, TypeCodecs.TEXT); + private static final TypeCodec> MAP_OF_TEXT_TO_BLOB = + TypeCodecs.mapOf(TypeCodecs.TEXT, TypeCodecs.BLOB); + /** + * The columns of the system table that are turned into entries in {@link + * RelationMetadata#getOptions()}. + */ + public static final ImmutableMap> OPTION_CODECS = + ImmutableMap.>builder() + .put("additional_write_policy", TypeCodecs.TEXT) + .put("bloom_filter_fp_chance", TypeCodecs.DOUBLE) + // In C* <= 2.2, this is a string, not a map (this is special-cased in parseOptions): + .put("caching", MAP_OF_TEXT_TO_TEXT) + .put("cdc", TypeCodecs.BOOLEAN) + .put("comment", TypeCodecs.TEXT) + .put("compaction", MAP_OF_TEXT_TO_TEXT) + // In C*<=2.2, must read from this column and another one called + // 'compaction_strategy_options' (this is special-cased in parseOptions): + .put("compaction_strategy_class", TypeCodecs.TEXT) + .put("compression", MAP_OF_TEXT_TO_TEXT) + // In C*<=2.2, must parse this column into a map (this is special-cased in parseOptions): + .put("compression_parameters", TypeCodecs.TEXT) + .put("crc_check_chance", TypeCodecs.DOUBLE) + .put("dclocal_read_repair_chance", TypeCodecs.DOUBLE) + .put("default_time_to_live", TypeCodecs.INT) + .put("extensions", MAP_OF_TEXT_TO_BLOB) + .put("gc_grace_seconds", TypeCodecs.INT) + .put("local_read_repair_chance", TypeCodecs.DOUBLE) + .put("max_index_interval", TypeCodecs.INT) + .put("memtable_flush_period_in_ms", TypeCodecs.INT) + .put("min_index_interval", TypeCodecs.INT) + .put("read_repair", TypeCodecs.TEXT) + .put("read_repair_chance", TypeCodecs.DOUBLE) + .put("speculative_retry", TypeCodecs.TEXT) + .build(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParser.java new file mode 100644 index 00000000000..beb02894a0d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParser.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; +import com.datastax.oss.driver.internal.core.metadata.schema.refresh.SchemaRefresh; + +/** + * The main entry point for system schema rows parsing. + * + *

Implementations must be thread-safe. + */ +public interface SchemaParser { + + /** + * Process the rows that this parser was initialized with, and creates a refresh that will be + * applied to the metadata. + * + * @see SchemaParserFactory#newInstance(SchemaRows) + */ + SchemaRefresh parse(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParserFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParserFactory.java new file mode 100644 index 00000000000..c61e8933c89 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParserFactory.java @@ -0,0 +1,22 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; + +public interface SchemaParserFactory { + SchemaParser newInstance(SchemaRows rows); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SimpleJsonParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SimpleJsonParser.java new file mode 100644 index 00000000000..8da63a9018c --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SimpleJsonParser.java @@ -0,0 +1,181 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.NotThreadSafe; + +/** + * A very simple json parser. The only reason we need to read json in the driver is because for + * historical reason Cassandra encodes a few properties using json in the schema and we need to + * decode them. + * + *

We however don't need a full-blown JSON library because: 1) we know we only need to decode + * string lists and string maps 2) we can basically assume the input is valid, we don't particularly + * have to bother about decoding exactly JSON as long as we at least decode what we need. 3) we + * don't really care much about performance, none of this is done in performance sensitive parts. + * + *

So instead of pulling a new dependency, we roll out our own very dumb parser. We should + * obviously not expose this publicly. + */ +@NotThreadSafe +public class SimpleJsonParser { + + private final String input; + private int idx; + + private SimpleJsonParser(String input) { + this.input = input; + } + + public static List parseStringList(String input) { + if (input == null || input.isEmpty()) { + return Collections.emptyList(); + } + + List output = new ArrayList<>(); + SimpleJsonParser parser = new SimpleJsonParser(input); + if (parser.nextCharSkipSpaces() != '[') { + throw new IllegalArgumentException("Not a JSON list: " + input); + } + + char c = parser.nextCharSkipSpaces(); + if (c == ']') { + return output; + } + + while (true) { + assert c == '"'; + output.add(parser.nextString()); + c = parser.nextCharSkipSpaces(); + if (c == ']') { + return output; + } + assert c == ','; + c = parser.nextCharSkipSpaces(); + } + } + + public static Map parseStringMap(String input) { + if (input == null || input.isEmpty()) { + return Collections.emptyMap(); + } + + Map output = new HashMap<>(); + SimpleJsonParser parser = new SimpleJsonParser(input); + if (parser.nextCharSkipSpaces() != '{') { + throw new IllegalArgumentException("Not a JSON map: " + input); + } + + char c = parser.nextCharSkipSpaces(); + if (c == '}') { + return output; + } + + while (true) { + assert c == '"'; + String key = parser.nextString(); + c = parser.nextCharSkipSpaces(); + assert c == ':'; + c = parser.nextCharSkipSpaces(); + assert c == '"'; + String value = parser.nextString(); + output.put(key, value); + c = parser.nextCharSkipSpaces(); + if (c == '}') { + return output; + } + assert c == ','; + c = parser.nextCharSkipSpaces(); + } + } + + /** Read the next char, the one at position idx, and advance ix. */ + private char nextChar() { + if (idx >= input.length()) { + throw new IllegalArgumentException("Invalid json input: " + input); + } + return input.charAt(idx++); + } + + /** Same as nextChar, except that it skips space characters (' ', '\t' and '\n'). */ + private char nextCharSkipSpaces() { + char c = nextChar(); + while (c == ' ' || c == '\t' || c == '\n') { + c = nextChar(); + } + return c; + } + + /** + * Reads a String, assuming idx is on the first character of the string (i.e. the one after the + * opening double-quote character). After the string has been read, idx will be on the first + * character after the closing double-quote. + */ + private String nextString() { + assert input.charAt(idx - 1) == '"' : "Char is '" + input.charAt(idx - 1) + '\''; + StringBuilder sb = new StringBuilder(); + while (true) { + char c = nextChar(); + switch (c) { + case '\n': + case '\r': + throw new IllegalArgumentException("Unterminated string"); + case '\\': + c = nextChar(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + sb.append((char) Integer.parseInt(input.substring(idx, idx + 4), 16)); + idx += 4; + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw new IllegalArgumentException("Illegal escape"); + } + break; + default: + if (c == '"') { + return sb.toString(); + } + sb.append(c); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/TableParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/TableParser.java new file mode 100644 index 00000000000..d7b09b7b11f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/TableParser.java @@ -0,0 +1,336 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.IndexKind; +import com.datastax.oss.driver.api.core.metadata.schema.IndexMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.MapType; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.schema.DefaultColumnMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.DefaultIndexMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.DefaultTableMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMultimap; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class TableParser extends RelationParser { + + private static final Logger LOG = LoggerFactory.getLogger(TableParser.class); + + public TableParser(SchemaRows rows, InternalDriverContext context) { + super(rows, context); + } + + public TableMetadata parseTable( + AdminRow tableRow, CqlIdentifier keyspaceId, Map userTypes) { + // Cassandra <= 2.2: + // CREATE TABLE system.schema_columnfamilies ( + // keyspace_name text, + // columnfamily_name text, + // bloom_filter_fp_chance double, + // caching text, + // cf_id uuid, + // column_aliases text, (2.1 only) + // comment text, + // compaction_strategy_class text, + // compaction_strategy_options text, + // comparator text, + // compression_parameters text, + // default_time_to_live int, + // default_validator text, + // dropped_columns map, + // gc_grace_seconds int, + // index_interval int, + // is_dense boolean, (2.1 only) + // key_aliases text, (2.1 only) + // key_validator text, + // local_read_repair_chance double, + // max_compaction_threshold int, + // max_index_interval int, + // memtable_flush_period_in_ms int, + // min_compaction_threshold int, + // min_index_interval int, + // read_repair_chance double, + // speculative_retry text, + // subcomparator text, + // type text, + // value_alias text, (2.1 only) + // PRIMARY KEY (keyspace_name, columnfamily_name) + // ) WITH CLUSTERING ORDER BY (columnfamily_name ASC) + // + // Cassandra 3.0: + // CREATE TABLE system_schema.tables ( + // keyspace_name text, + // table_name text, + // bloom_filter_fp_chance double, + // caching frozen>, + // cdc boolean, + // comment text, + // compaction frozen>, + // compression frozen>, + // crc_check_chance double, + // dclocal_read_repair_chance double, + // default_time_to_live int, + // extensions frozen>, + // flags frozen>, + // gc_grace_seconds int, + // id uuid, + // max_index_interval int, + // memtable_flush_period_in_ms int, + // min_index_interval int, + // read_repair_chance double, + // speculative_retry text, + // PRIMARY KEY (keyspace_name, table_name) + // ) WITH CLUSTERING ORDER BY (table_name ASC) + CqlIdentifier tableId = + CqlIdentifier.fromInternal( + tableRow.getString( + tableRow.contains("table_name") ? "table_name" : "columnfamily_name")); + + UUID uuid = (tableRow.contains("id")) ? tableRow.getUuid("id") : tableRow.getUuid("cf_id"); + + List rawColumns = + RawColumn.toRawColumns( + rows.columns().getOrDefault(keyspaceId, ImmutableMultimap.of()).get(tableId), + keyspaceId, + userTypes); + if (rawColumns.isEmpty()) { + LOG.warn( + "[{}] Processing TABLE refresh for {}.{} but found no matching rows, skipping", + logPrefix, + keyspaceId, + tableId); + return null; + } + + boolean isCompactStorage; + if (tableRow.contains("flags")) { + Set flags = tableRow.getSetOfString("flags"); + boolean isDense = flags.contains("dense"); + boolean isSuper = flags.contains("super"); + boolean isCompound = flags.contains("compound"); + isCompactStorage = isSuper || isDense || !isCompound; + boolean isStaticCompact = !isSuper && !isDense && !isCompound; + if (isStaticCompact) { + RawColumn.pruneStaticCompactTableColumns(rawColumns); + } else if (isDense) { + RawColumn.pruneDenseTableColumnsV3(rawColumns); + } + } else { + boolean isDense = tableRow.getBoolean("is_dense"); + if (isDense) { + RawColumn.pruneDenseTableColumnsV2(rawColumns); + } + DataTypeClassNameCompositeParser.ParseResult comparator = + new DataTypeClassNameCompositeParser() + .parseWithComposite(tableRow.getString("comparator"), keyspaceId, userTypes, context); + isCompactStorage = isDense || !comparator.isComposite; + } + + Collections.sort(rawColumns); + ImmutableMap.Builder allColumnsBuilder = ImmutableMap.builder(); + ImmutableList.Builder partitionKeyBuilder = ImmutableList.builder(); + ImmutableMap.Builder clusteringColumnsBuilder = + ImmutableMap.builder(); + ImmutableMap.Builder indexesBuilder = ImmutableMap.builder(); + + for (RawColumn raw : rawColumns) { + DataType dataType = rows.dataTypeParser().parse(keyspaceId, raw.dataType, userTypes, context); + ColumnMetadata column = + new DefaultColumnMetadata( + keyspaceId, tableId, raw.name, dataType, raw.kind.equals(RawColumn.KIND_STATIC)); + switch (raw.kind) { + case RawColumn.KIND_PARTITION_KEY: + partitionKeyBuilder.add(column); + break; + case RawColumn.KIND_CLUSTERING_COLUMN: + clusteringColumnsBuilder.put( + column, raw.reversed ? ClusteringOrder.DESC : ClusteringOrder.ASC); + break; + default: + // nothing to do + } + allColumnsBuilder.put(column.getName(), column); + + IndexMetadata index = buildLegacyIndex(raw, column); + if (index != null) { + indexesBuilder.put(index.getName(), index); + } + } + + Map options; + try { + options = parseOptions(tableRow); + } catch (Exception e) { + // Options change the most often, so be especially lenient if anything goes wrong. + Loggers.warnWithException( + LOG, + "[{}] Error while parsing options for {}.{}, getOptions() will be empty", + logPrefix, + keyspaceId, + tableId, + e); + options = Collections.emptyMap(); + } + + Collection indexRows = + rows.indexes().getOrDefault(keyspaceId, ImmutableMultimap.of()).get(tableId); + for (AdminRow indexRow : indexRows) { + IndexMetadata index = buildModernIndex(keyspaceId, tableId, indexRow); + indexesBuilder.put(index.getName(), index); + } + + return new DefaultTableMetadata( + keyspaceId, + tableId, + uuid, + isCompactStorage, + false, + partitionKeyBuilder.build(), + clusteringColumnsBuilder.build(), + allColumnsBuilder.build(), + options, + indexesBuilder.build()); + } + + TableMetadata parseVirtualTable( + AdminRow tableRow, CqlIdentifier keyspaceId, Map userTypes) { + + CqlIdentifier tableId = CqlIdentifier.fromInternal(tableRow.getString("table_name")); + + List rawColumns = + RawColumn.toRawColumns( + rows.virtualColumns().getOrDefault(keyspaceId, ImmutableMultimap.of()).get(tableId), + keyspaceId, + userTypes); + if (rawColumns.isEmpty()) { + LOG.warn( + "[{}] Processing TABLE refresh for {}.{} but found no matching rows, skipping", + logPrefix, + keyspaceId, + tableId); + return null; + } + + Collections.sort(rawColumns); + ImmutableMap.Builder allColumnsBuilder = ImmutableMap.builder(); + ImmutableList.Builder partitionKeyBuilder = ImmutableList.builder(); + ImmutableMap.Builder clusteringColumnsBuilder = + ImmutableMap.builder(); + + for (RawColumn raw : rawColumns) { + DataType dataType = rows.dataTypeParser().parse(keyspaceId, raw.dataType, userTypes, context); + ColumnMetadata column = + new DefaultColumnMetadata( + keyspaceId, tableId, raw.name, dataType, raw.kind.equals(RawColumn.KIND_STATIC)); + switch (raw.kind) { + case RawColumn.KIND_PARTITION_KEY: + partitionKeyBuilder.add(column); + break; + case RawColumn.KIND_CLUSTERING_COLUMN: + clusteringColumnsBuilder.put( + column, raw.reversed ? ClusteringOrder.DESC : ClusteringOrder.ASC); + break; + default: + } + + allColumnsBuilder.put(column.getName(), column); + } + + return new DefaultTableMetadata( + keyspaceId, + tableId, + null, + false, + true, + partitionKeyBuilder.build(), + clusteringColumnsBuilder.build(), + allColumnsBuilder.build(), + Collections.emptyMap(), + Collections.emptyMap()); + } + + // In C*<=2.2, index information is stored alongside the column. + private IndexMetadata buildLegacyIndex(RawColumn raw, ColumnMetadata column) { + if (raw.indexName == null) { + return null; + } + return new DefaultIndexMetadata( + column.getKeyspace(), + column.getParent(), + CqlIdentifier.fromInternal(raw.indexName), + IndexKind.valueOf(raw.indexType), + buildLegacyIndexTarget(column, raw.indexOptions), + raw.indexOptions); + } + + private static String buildLegacyIndexTarget(ColumnMetadata column, Map options) { + String columnName = column.getName().asCql(true); + DataType columnType = column.getType(); + if (options.containsKey("index_keys")) { + return String.format("keys(%s)", columnName); + } + if (options.containsKey("index_keys_and_values")) { + return String.format("entries(%s)", columnName); + } + if ((columnType instanceof ListType && ((ListType) columnType).isFrozen()) + || (columnType instanceof SetType && ((SetType) columnType).isFrozen()) + || (columnType instanceof MapType && ((MapType) columnType).isFrozen())) { + return String.format("full(%s)", columnName); + } + // Note: the keyword 'values' is not accepted as a valid index target function until 3.0 + return columnName; + } + + // In C*>=3.0, index information is stored in a dedicated table: + // CREATE TABLE system_schema.indexes ( + // keyspace_name text, + // table_name text, + // index_name text, + // kind text, + // options frozen>, + // PRIMARY KEY (keyspace_name, table_name, index_name) + // ) WITH CLUSTERING ORDER BY (table_name ASC, index_name ASC) + private IndexMetadata buildModernIndex( + CqlIdentifier keyspaceId, CqlIdentifier tableId, AdminRow row) { + CqlIdentifier name = CqlIdentifier.fromInternal(row.getString("index_name")); + IndexKind kind = IndexKind.valueOf(row.getString("kind")); + Map options = row.getMapOfStringToString("options"); + String target = options.get("target"); + return new DefaultIndexMetadata(keyspaceId, tableId, name, kind, target, options); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/UserDefinedTypeParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/UserDefinedTypeParser.java new file mode 100644 index 00000000000..f04b6c9a807 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/UserDefinedTypeParser.java @@ -0,0 +1,162 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.MapType; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.type.DefaultUserDefinedType; +import com.datastax.oss.driver.internal.core.util.DirectedGraph; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class UserDefinedTypeParser { + private final DataTypeParser dataTypeParser; + private final InternalDriverContext context; + + public UserDefinedTypeParser(DataTypeParser dataTypeParser, InternalDriverContext context) { + this.dataTypeParser = dataTypeParser; + this.context = context; + } + + /** + * Contrary to other element parsers, this one processes all the types of a keyspace in one go. + * UDTs can depend on each other, but the system table returns them in alphabetical order. In + * order to properly build the definitions, we need to do a topological sort of the rows first, so + * that each type is parsed after its dependencies. + */ + public Map parse( + Collection typeRows, CqlIdentifier keyspaceId) { + if (typeRows.isEmpty()) { + return Collections.emptyMap(); + } else { + Map types = new LinkedHashMap<>(); + for (AdminRow row : topologicalSort(typeRows, keyspaceId)) { + UserDefinedType type = parseType(row, keyspaceId, types); + types.put(type.getName(), type); + } + return ImmutableMap.copyOf(types); + } + } + + @VisibleForTesting + Map parse(CqlIdentifier keyspaceId, AdminRow... typeRows) { + return parse(Arrays.asList(typeRows), keyspaceId); + } + + private List topologicalSort(Collection typeRows, CqlIdentifier keyspaceId) { + if (typeRows.size() == 1) { + AdminRow row = typeRows.iterator().next(); + return Collections.singletonList(row); + } else { + DirectedGraph graph = new DirectedGraph<>(typeRows); + for (AdminRow dependent : typeRows) { + for (AdminRow dependency : typeRows) { + if (dependent != dependency && dependsOn(dependent, dependency, keyspaceId)) { + // Edges mean "is depended upon by"; we want the types with no dependencies to come + // first in the sort. + graph.addEdge(dependency, dependent); + } + } + } + return graph.topologicalSort(); + } + } + + private boolean dependsOn(AdminRow dependent, AdminRow dependency, CqlIdentifier keyspaceId) { + CqlIdentifier dependencyId = CqlIdentifier.fromInternal(dependency.getString("type_name")); + for (String fieldTypeName : dependent.getListOfString("field_types")) { + DataType fieldType = dataTypeParser.parse(keyspaceId, fieldTypeName, null, context); + if (references(fieldType, dependencyId)) { + return true; + } + } + return false; + } + + private boolean references(DataType dependent, CqlIdentifier dependency) { + if (dependent instanceof UserDefinedType) { + UserDefinedType userType = (UserDefinedType) dependent; + return userType.getName().equals(dependency); + } else if (dependent instanceof ListType) { + ListType listType = (ListType) dependent; + return references(listType.getElementType(), dependency); + } else if (dependent instanceof SetType) { + SetType setType = (SetType) dependent; + return references(setType.getElementType(), dependency); + } else if (dependent instanceof MapType) { + MapType mapType = (MapType) dependent; + return references(mapType.getKeyType(), dependency) + || references(mapType.getValueType(), dependency); + } else if (dependent instanceof TupleType) { + TupleType tupleType = (TupleType) dependent; + for (DataType componentType : tupleType.getComponentTypes()) { + if (references(componentType, dependency)) { + return true; + } + } + } + return false; + } + + private UserDefinedType parseType( + AdminRow row, + CqlIdentifier keyspaceId, + Map userDefinedTypes) { + // Cassandra < 3.0: + // CREATE TABLE system.schema_usertypes ( + // keyspace_name text, + // type_name text, + // field_names list, + // field_types list, + // PRIMARY KEY (keyspace_name, type_name) + // ) WITH CLUSTERING ORDER BY (type_name ASC) + // + // Cassandra >= 3.0: + // CREATE TABLE system_schema.types ( + // keyspace_name text, + // type_name text, + // field_names frozen>, + // field_types frozen>, + // PRIMARY KEY (keyspace_name, type_name) + // ) WITH CLUSTERING ORDER BY (type_name ASC) + CqlIdentifier name = CqlIdentifier.fromInternal(row.getString("type_name")); + List fieldNames = + ImmutableList.copyOf( + Lists.transform(row.getListOfString("field_names"), CqlIdentifier::fromInternal)); + List fieldTypes = + dataTypeParser.parse( + keyspaceId, row.getListOfString("field_types"), userDefinedTypes, context); + + return new DefaultUserDefinedType(keyspaceId, name, false, fieldNames, fieldTypes, context); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/ViewParser.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/ViewParser.java new file mode 100644 index 00000000000..48bdac0a07e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/ViewParser.java @@ -0,0 +1,154 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.ViewMetadata; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.schema.DefaultColumnMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.DefaultViewMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.shaded.guava.common.base.MoreObjects; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMultimap; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class ViewParser extends RelationParser { + + private static final Logger LOG = LoggerFactory.getLogger(ViewParser.class); + + public ViewParser(SchemaRows rows, InternalDriverContext context) { + super(rows, context); + } + + public ViewMetadata parseView( + AdminRow viewRow, CqlIdentifier keyspaceId, Map userTypes) { + // Cassandra 3.0 (no views in earlier versions): + // CREATE TABLE system_schema.views ( + // keyspace_name text, + // view_name text, + // base_table_id uuid, + // base_table_name text, + // bloom_filter_fp_chance double, + // caching frozen>, + // cdc boolean, + // comment text, + // compaction frozen>, + // compression frozen>, + // crc_check_chance double, + // dclocal_read_repair_chance double, + // default_time_to_live int, + // extensions frozen>, + // gc_grace_seconds int, + // id uuid, + // include_all_columns boolean, + // max_index_interval int, + // memtable_flush_period_in_ms int, + // min_index_interval int, + // read_repair_chance double, + // speculative_retry text, + // where_clause text, + // PRIMARY KEY (keyspace_name, view_name) + // ) WITH CLUSTERING ORDER BY (view_name ASC) + CqlIdentifier viewId = CqlIdentifier.fromInternal(viewRow.getString("view_name")); + + UUID uuid = viewRow.getUuid("id"); + CqlIdentifier baseTableId = CqlIdentifier.fromInternal(viewRow.getString("base_table_name")); + boolean includesAllColumns = + MoreObjects.firstNonNull(viewRow.getBoolean("include_all_columns"), false); + String whereClause = viewRow.getString("where_clause"); + + List rawColumns = + RawColumn.toRawColumns( + rows.columns().getOrDefault(keyspaceId, ImmutableMultimap.of()).get(viewId), + keyspaceId, + userTypes); + if (rawColumns.isEmpty()) { + LOG.warn( + "[{}] Processing VIEW refresh for {}.{} but found no matching rows, skipping", + logPrefix, + keyspaceId, + viewId); + return null; + } + + Collections.sort(rawColumns); + ImmutableMap.Builder allColumnsBuilder = ImmutableMap.builder(); + ImmutableList.Builder partitionKeyBuilder = ImmutableList.builder(); + ImmutableMap.Builder clusteringColumnsBuilder = + ImmutableMap.builder(); + + for (RawColumn raw : rawColumns) { + DataType dataType = rows.dataTypeParser().parse(keyspaceId, raw.dataType, userTypes, context); + ColumnMetadata column = + new DefaultColumnMetadata( + keyspaceId, viewId, raw.name, dataType, raw.kind.equals(RawColumn.KIND_STATIC)); + switch (raw.kind) { + case RawColumn.KIND_PARTITION_KEY: + partitionKeyBuilder.add(column); + break; + case RawColumn.KIND_CLUSTERING_COLUMN: + clusteringColumnsBuilder.put( + column, raw.reversed ? ClusteringOrder.DESC : ClusteringOrder.ASC); + break; + default: + // nothing to do + } + allColumnsBuilder.put(column.getName(), column); + } + + Map options; + try { + options = parseOptions(viewRow); + } catch (Exception e) { + // Options change the most often, so be especially lenient if anything goes wrong. + Loggers.warnWithException( + LOG, + "[{}] Error while parsing options for {}.{}, getOptions() will be empty", + logPrefix, + keyspaceId, + viewId, + e); + options = Collections.emptyMap(); + } + + return new DefaultViewMetadata( + keyspaceId, + viewId, + baseTableId, + includesAllColumns, + whereClause, + uuid, + partitionKeyBuilder.build(), + clusteringColumnsBuilder.build(), + allColumnsBuilder.build(), + options); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra21SchemaQueries.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra21SchemaQueries.java new file mode 100644 index 00000000000..556c9c58b6b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra21SchemaQueries.java @@ -0,0 +1,89 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class Cassandra21SchemaQueries extends CassandraSchemaQueries { + public Cassandra21SchemaQueries( + DriverChannel channel, + CompletableFuture refreshFuture, + DriverExecutionProfile config, + String logPrefix) { + super(channel, false, refreshFuture, config, logPrefix); + } + + @Override + protected String selectKeyspacesQuery() { + return "SELECT * FROM system.schema_keyspaces"; + } + + @Override + protected String selectTablesQuery() { + return "SELECT * FROM system.schema_columnfamilies"; + } + + @Override + protected Optional selectViewsQuery() { + return Optional.empty(); + } + + @Override + protected Optional selectIndexesQuery() { + return Optional.empty(); + } + + @Override + protected String selectColumnsQuery() { + return "SELECT * FROM system.schema_columns"; + } + + @Override + protected String selectTypesQuery() { + return "SELECT * FROM system.schema_usertypes"; + } + + @Override + protected Optional selectFunctionsQuery() { + return Optional.empty(); + } + + @Override + protected Optional selectAggregatesQuery() { + return Optional.empty(); + } + + @Override + protected Optional selectVirtualKeyspacesQuery() { + return Optional.empty(); + } + + @Override + protected Optional selectVirtualTablesQuery() { + return Optional.empty(); + } + + @Override + protected Optional selectVirtualColumnsQuery() { + return Optional.empty(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra22SchemaQueries.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra22SchemaQueries.java new file mode 100644 index 00000000000..130599b86e2 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra22SchemaQueries.java @@ -0,0 +1,89 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class Cassandra22SchemaQueries extends CassandraSchemaQueries { + public Cassandra22SchemaQueries( + DriverChannel channel, + CompletableFuture refreshFuture, + DriverExecutionProfile config, + String logPrefix) { + super(channel, false, refreshFuture, config, logPrefix); + } + + @Override + protected String selectKeyspacesQuery() { + return "SELECT * FROM system.schema_keyspaces"; + } + + @Override + protected String selectTablesQuery() { + return "SELECT * FROM system.schema_columnfamilies"; + } + + @Override + protected Optional selectViewsQuery() { + return Optional.empty(); + } + + @Override + protected Optional selectIndexesQuery() { + return Optional.empty(); + } + + @Override + protected String selectColumnsQuery() { + return "SELECT * FROM system.schema_columns"; + } + + @Override + protected String selectTypesQuery() { + return "SELECT * FROM system.schema_usertypes"; + } + + @Override + protected Optional selectFunctionsQuery() { + return Optional.of("SELECT * FROM system.schema_functions"); + } + + @Override + protected Optional selectAggregatesQuery() { + return Optional.of("SELECT * FROM system.schema_aggregates"); + } + + @Override + protected Optional selectVirtualKeyspacesQuery() { + return Optional.empty(); + } + + @Override + protected Optional selectVirtualTablesQuery() { + return Optional.empty(); + } + + @Override + protected Optional selectVirtualColumnsQuery() { + return Optional.empty(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra3SchemaQueries.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra3SchemaQueries.java new file mode 100644 index 00000000000..c2c97873624 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra3SchemaQueries.java @@ -0,0 +1,89 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class Cassandra3SchemaQueries extends CassandraSchemaQueries { + public Cassandra3SchemaQueries( + DriverChannel channel, + CompletableFuture refreshFuture, + DriverExecutionProfile config, + String logPrefix) { + super(channel, true, refreshFuture, config, logPrefix); + } + + @Override + protected String selectKeyspacesQuery() { + return "SELECT * FROM system_schema.keyspaces"; + } + + @Override + protected String selectTablesQuery() { + return "SELECT * FROM system_schema.tables"; + } + + @Override + protected Optional selectViewsQuery() { + return Optional.of("SELECT * FROM system_schema.views"); + } + + @Override + protected Optional selectIndexesQuery() { + return Optional.of("SELECT * FROM system_schema.indexes"); + } + + @Override + protected String selectColumnsQuery() { + return "SELECT * FROM system_schema.columns"; + } + + @Override + protected String selectTypesQuery() { + return "SELECT * FROM system_schema.types"; + } + + @Override + protected Optional selectFunctionsQuery() { + return Optional.of("SELECT * FROM system_schema.functions"); + } + + @Override + protected Optional selectAggregatesQuery() { + return Optional.of("SELECT * FROM system_schema.aggregates"); + } + + @Override + protected Optional selectVirtualKeyspacesQuery() { + return Optional.empty(); + } + + @Override + protected Optional selectVirtualTablesQuery() { + return Optional.empty(); + } + + @Override + protected Optional selectVirtualColumnsQuery() { + return Optional.empty(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra4SchemaQueries.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra4SchemaQueries.java new file mode 100644 index 00000000000..641a97119b9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra4SchemaQueries.java @@ -0,0 +1,49 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class Cassandra4SchemaQueries extends Cassandra3SchemaQueries { + public Cassandra4SchemaQueries( + DriverChannel channel, + CompletableFuture refreshFuture, + DriverExecutionProfile config, + String logPrefix) { + super(channel, refreshFuture, config, logPrefix); + } + + @Override + protected Optional selectVirtualKeyspacesQuery() { + return Optional.of("SELECT * FROM system_virtual_schema.keyspaces"); + } + + @Override + protected Optional selectVirtualTablesQuery() { + return Optional.of("SELECT * FROM system_virtual_schema.tables"); + } + + @Override + protected Optional selectVirtualColumnsQuery() { + return Optional.of("SELECT * FROM system_virtual_schema.columns"); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java new file mode 100644 index 00000000000..8aa4ebe8f83 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaQueries.java @@ -0,0 +1,220 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRequestHandler; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.NanoTime; +import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import io.netty.util.concurrent.EventExecutor; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public abstract class CassandraSchemaQueries implements SchemaQueries { + + private static final Logger LOG = LoggerFactory.getLogger(CassandraSchemaQueries.class); + + private final DriverChannel channel; + private final EventExecutor adminExecutor; + private final boolean isCassandraV3; + private final String logPrefix; + private final Duration timeout; + private final int pageSize; + private final String whereClause; + // The future we return from execute, completes when all the queries are done. + private final CompletableFuture schemaRowsFuture = new CompletableFuture<>(); + // A future that completes later, when the whole refresh is done. We just store it here to pass it + // down to the next step. + public final CompletableFuture refreshFuture; + private final long startTimeNs = System.nanoTime(); + + // All non-final fields are accessed exclusively on adminExecutor + private CassandraSchemaRows.Builder schemaRowsBuilder; + private int pendingQueries; + + protected CassandraSchemaQueries( + DriverChannel channel, + boolean isCassandraV3, + CompletableFuture refreshFuture, + DriverExecutionProfile config, + String logPrefix) { + this.channel = channel; + this.adminExecutor = channel.eventLoop(); + this.isCassandraV3 = isCassandraV3; + this.refreshFuture = refreshFuture; + this.logPrefix = logPrefix; + this.timeout = config.getDuration(DefaultDriverOption.METADATA_SCHEMA_REQUEST_TIMEOUT); + this.pageSize = config.getInt(DefaultDriverOption.METADATA_SCHEMA_REQUEST_PAGE_SIZE); + + List refreshedKeyspaces = + config.getStringList( + DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, Collections.emptyList()); + this.whereClause = buildWhereClause(refreshedKeyspaces); + } + + private static String buildWhereClause(List refreshedKeyspaces) { + if (refreshedKeyspaces.isEmpty()) { + return ""; + } else { + StringBuilder builder = new StringBuilder(" WHERE keyspace_name in ("); + boolean first = true; + for (String keyspace : refreshedKeyspaces) { + if (first) { + first = false; + } else { + builder.append(","); + } + builder.append('\'').append(keyspace).append('\''); + } + return builder.append(")").toString(); + } + } + + protected abstract String selectKeyspacesQuery(); + + protected abstract Optional selectVirtualKeyspacesQuery(); + + protected abstract String selectTablesQuery(); + + protected abstract Optional selectVirtualTablesQuery(); + + protected abstract Optional selectViewsQuery(); + + protected abstract Optional selectIndexesQuery(); + + protected abstract String selectColumnsQuery(); + + protected abstract Optional selectVirtualColumnsQuery(); + + protected abstract String selectTypesQuery(); + + protected abstract Optional selectFunctionsQuery(); + + protected abstract Optional selectAggregatesQuery(); + + @Override + public CompletionStage execute() { + RunOrSchedule.on(adminExecutor, this::executeOnAdminExecutor); + return schemaRowsFuture; + } + + private void executeOnAdminExecutor() { + assert adminExecutor.inEventLoop(); + + schemaRowsBuilder = new CassandraSchemaRows.Builder(isCassandraV3, refreshFuture, logPrefix); + + query(selectKeyspacesQuery() + whereClause, schemaRowsBuilder::withKeyspaces, true); + query(selectTypesQuery() + whereClause, schemaRowsBuilder::withTypes, true); + query(selectTablesQuery() + whereClause, schemaRowsBuilder::withTables, true); + query(selectColumnsQuery() + whereClause, schemaRowsBuilder::withColumns, true); + selectIndexesQuery() + .ifPresent(select -> query(select + whereClause, schemaRowsBuilder::withIndexes, true)); + selectViewsQuery() + .ifPresent(select -> query(select + whereClause, schemaRowsBuilder::withViews, true)); + selectFunctionsQuery() + .ifPresent(select -> query(select + whereClause, schemaRowsBuilder::withFunctions, true)); + selectAggregatesQuery() + .ifPresent(select -> query(select + whereClause, schemaRowsBuilder::withAggregates, true)); + selectVirtualKeyspacesQuery() + .ifPresent( + select -> query(select + whereClause, schemaRowsBuilder::withVirtualKeyspaces, false)); + selectVirtualTablesQuery() + .ifPresent( + select -> query(select + whereClause, schemaRowsBuilder::withVirtualTables, false)); + selectVirtualColumnsQuery() + .ifPresent( + select -> query(select + whereClause, schemaRowsBuilder::withVirtualColumns, false)); + } + + private void query( + String queryString, + Function, CassandraSchemaRows.Builder> builderUpdater, + boolean warnIfMissing) { + assert adminExecutor.inEventLoop(); + + pendingQueries += 1; + query(queryString) + .whenCompleteAsync( + (result, error) -> handleResult(result, error, builderUpdater, warnIfMissing), + adminExecutor); + } + + @VisibleForTesting + protected CompletionStage query(String query) { + return AdminRequestHandler.query(channel, query, timeout, pageSize, logPrefix).start(); + } + + /** + * @param warnIfMissing whether to log a warning if the queried table does not exist: some DDAC + * versions report release_version > 4, but don't have a system_virtual_schema keyspace, so we + * want to ignore those errors silently. + */ + private void handleResult( + AdminResult result, + Throwable error, + Function, CassandraSchemaRows.Builder> builderUpdater, + boolean warnIfMissing) { + if (error != null) { + if (warnIfMissing || !error.getMessage().contains("does not exist")) { + Loggers.warnWithException( + LOG, + "[{}] Error during schema refresh, new metadata might be incomplete", + logPrefix, + error); + } + // Proceed without the results of this query, the rest of the schema refresh will run on a + // "best effort" basis + markQueryComplete(); + } else { + // Store the rows of the current page in the builder + schemaRowsBuilder = builderUpdater.apply(result); + if (result.hasNextPage()) { + result + .nextPage() + .whenCompleteAsync( + (nextResult, nextError) -> + handleResult(nextResult, nextError, builderUpdater, warnIfMissing), + adminExecutor); + } else { + markQueryComplete(); + } + } + } + + private void markQueryComplete() { + pendingQueries -= 1; + if (pendingQueries == 0) { + LOG.debug("[{}] Schema queries took {}", logPrefix, NanoTime.formatTimeSince(startTimeNs)); + schemaRowsFuture.complete(schemaRowsBuilder.build()); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaRows.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaRows.java new file mode 100644 index 00000000000..49a49764021 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/CassandraSchemaRows.java @@ -0,0 +1,311 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.DataTypeClassNameParser; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.DataTypeCqlNameParser; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.DataTypeParser; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableListMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.Multimap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import net.jcip.annotations.Immutable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Immutable +public class CassandraSchemaRows implements SchemaRows { + + private final DataTypeParser dataTypeParser; + private final CompletableFuture refreshFuture; + private final List keyspaces; + private final List virtualKeyspaces; + private final Multimap tables; + private final Multimap virtualTables; + private final Multimap views; + private final Multimap types; + private final Multimap functions; + private final Multimap aggregates; + private final Map> columns; + private final Map> virtualColumns; + private final Map> indexes; + + private CassandraSchemaRows( + boolean isCassandraV3, + CompletableFuture refreshFuture, + List keyspaces, + List virtualKeyspaces, + Multimap tables, + Multimap virtualTables, + Multimap views, + Map> columns, + Map> virtualColumns, + Map> indexes, + Multimap types, + Multimap functions, + Multimap aggregates) { + this.dataTypeParser = + isCassandraV3 ? new DataTypeCqlNameParser() : new DataTypeClassNameParser(); + this.refreshFuture = refreshFuture; + this.keyspaces = keyspaces; + this.virtualKeyspaces = virtualKeyspaces; + this.tables = tables; + this.virtualTables = virtualTables; + this.views = views; + this.columns = columns; + this.virtualColumns = virtualColumns; + this.indexes = indexes; + this.types = types; + this.functions = functions; + this.aggregates = aggregates; + } + + @Override + public DataTypeParser dataTypeParser() { + return dataTypeParser; + } + + @Override + public CompletableFuture refreshFuture() { + return refreshFuture; + } + + @Override + public List keyspaces() { + return keyspaces; + } + + @Override + public List virtualKeyspaces() { + return virtualKeyspaces; + } + + @Override + public Multimap tables() { + return tables; + } + + @Override + public Multimap virtualTables() { + return virtualTables; + } + + @Override + public Multimap views() { + return views; + } + + @Override + public Multimap types() { + return types; + } + + @Override + public Multimap functions() { + return functions; + } + + @Override + public Multimap aggregates() { + return aggregates; + } + + @Override + public Map> columns() { + return columns; + } + + @Override + public Map> virtualColumns() { + return virtualColumns; + } + + @Override + public Map> indexes() { + return indexes; + } + + public static class Builder { + private static final Logger LOG = LoggerFactory.getLogger(Builder.class); + + private final boolean isCassandraV3; + private final CompletableFuture refreshFuture; + private final String tableNameColumn; + private final String logPrefix; + private final ImmutableList.Builder keyspacesBuilder = ImmutableList.builder(); + private final ImmutableList.Builder virtualKeyspacesBuilder = ImmutableList.builder(); + private final ImmutableMultimap.Builder tablesBuilder = + ImmutableListMultimap.builder(); + private final ImmutableMultimap.Builder virtualTablesBuilder = + ImmutableListMultimap.builder(); + private final ImmutableMultimap.Builder viewsBuilder = + ImmutableListMultimap.builder(); + private final ImmutableMultimap.Builder typesBuilder = + ImmutableListMultimap.builder(); + private final ImmutableMultimap.Builder functionsBuilder = + ImmutableListMultimap.builder(); + private final ImmutableMultimap.Builder aggregatesBuilder = + ImmutableListMultimap.builder(); + private final Map> + columnsBuilders = new LinkedHashMap<>(); + private final Map> + virtualColumnsBuilders = new LinkedHashMap<>(); + private final Map> + indexesBuilders = new LinkedHashMap<>(); + + public Builder( + boolean isCassandraV3, CompletableFuture refreshFuture, String logPrefix) { + this.isCassandraV3 = isCassandraV3; + this.refreshFuture = refreshFuture; + this.logPrefix = logPrefix; + this.tableNameColumn = isCassandraV3 ? "table_name" : "columnfamily_name"; + } + + public Builder withKeyspaces(Iterable rows) { + keyspacesBuilder.addAll(rows); + return this; + } + + public Builder withVirtualKeyspaces(Iterable rows) { + virtualKeyspacesBuilder.addAll(rows); + return this; + } + + public Builder withTables(Iterable rows) { + for (AdminRow row : rows) { + putByKeyspace(row, tablesBuilder); + } + return this; + } + + public Builder withVirtualTables(Iterable rows) { + for (AdminRow row : rows) { + putByKeyspace(row, virtualTablesBuilder); + } + return this; + } + + public Builder withViews(Iterable rows) { + for (AdminRow row : rows) { + putByKeyspace(row, viewsBuilder); + } + return this; + } + + public Builder withTypes(Iterable rows) { + for (AdminRow row : rows) { + putByKeyspace(row, typesBuilder); + } + return this; + } + + public Builder withFunctions(Iterable rows) { + for (AdminRow row : rows) { + putByKeyspace(row, functionsBuilder); + } + return this; + } + + public Builder withAggregates(Iterable rows) { + for (AdminRow row : rows) { + putByKeyspace(row, aggregatesBuilder); + } + return this; + } + + public Builder withColumns(Iterable rows) { + for (AdminRow row : rows) { + putByKeyspaceAndTable(row, columnsBuilders); + } + return this; + } + + public Builder withVirtualColumns(Iterable rows) { + for (AdminRow row : rows) { + putByKeyspaceAndTable(row, virtualColumnsBuilders); + } + return this; + } + + public Builder withIndexes(Iterable rows) { + for (AdminRow row : rows) { + putByKeyspaceAndTable(row, indexesBuilders); + } + return this; + } + + private void putByKeyspace( + AdminRow row, ImmutableMultimap.Builder builder) { + String keyspace = row.getString("keyspace_name"); + if (keyspace == null) { + LOG.warn("[{}] Skipping system row with missing keyspace name", logPrefix); + } else { + builder.put(CqlIdentifier.fromInternal(keyspace), row); + } + } + + private void putByKeyspaceAndTable( + AdminRow row, + Map> builders) { + String keyspace = row.getString("keyspace_name"); + String table = row.getString(tableNameColumn); + if (keyspace == null) { + LOG.warn("[{}] Skipping system row with missing keyspace name", logPrefix); + } else if (table == null) { + LOG.warn("[{}] Skipping system row with missing table name", logPrefix); + } else { + ImmutableMultimap.Builder builder = + builders.computeIfAbsent( + CqlIdentifier.fromInternal(keyspace), s -> ImmutableListMultimap.builder()); + builder.put(CqlIdentifier.fromInternal(table), row); + } + } + + public CassandraSchemaRows build() { + return new CassandraSchemaRows( + isCassandraV3, + refreshFuture, + keyspacesBuilder.build(), + virtualKeyspacesBuilder.build(), + tablesBuilder.build(), + virtualTablesBuilder.build(), + viewsBuilder.build(), + build(columnsBuilders), + build(virtualColumnsBuilders), + build(indexesBuilders), + typesBuilder.build(), + functionsBuilder.build(), + aggregatesBuilder.build()); + } + + private static Map> build( + Map> builders) { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (Map.Entry> entry : builders.entrySet()) { + builder.put(entry.getKey(), entry.getValue().build()); + } + return builder.build(); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/DefaultSchemaQueriesFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/DefaultSchemaQueriesFactory.java new file mode 100644 index 00000000000..aee7ccaa5cb --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/DefaultSchemaQueriesFactory.java @@ -0,0 +1,87 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import java.util.concurrent.CompletableFuture; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class DefaultSchemaQueriesFactory implements SchemaQueriesFactory { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultSchemaQueriesFactory.class); + + protected final InternalDriverContext context; + protected final String logPrefix; + + public DefaultSchemaQueriesFactory(InternalDriverContext context) { + this.context = context; + this.logPrefix = context.getSessionName(); + } + + @Override + public SchemaQueries newInstance(CompletableFuture refreshFuture) { + DriverChannel channel = context.getControlConnection().channel(); + if (channel == null || channel.closeFuture().isDone()) { + throw new IllegalStateException("Control channel not available, aborting schema refresh"); + } + Node node = + context + .getMetadataManager() + .getMetadata() + .findNode(channel.getEndPoint()) + .orElseThrow( + () -> + new IllegalStateException( + "Could not find control node metadata " + + channel.getEndPoint() + + ", aborting schema refresh")); + return newInstance(node, channel, refreshFuture); + } + + protected SchemaQueries newInstance( + Node node, DriverChannel channel, CompletableFuture refreshFuture) { + Version version = node.getCassandraVersion(); + if (version == null) { + LOG.warn( + "[{}] Cassandra version missing for {}, defaulting to {}", + logPrefix, + node, + Version.V3_0_0); + version = Version.V3_0_0; + } else { + version = version.nextStable(); + } + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + LOG.debug("[{}] Sending schema queries to {} with version {}", logPrefix, node, version); + if (version.compareTo(Version.V2_2_0) < 0) { + return new Cassandra21SchemaQueries(channel, refreshFuture, config, logPrefix); + } else if (version.compareTo(Version.V3_0_0) < 0) { + return new Cassandra22SchemaQueries(channel, refreshFuture, config, logPrefix); + } else if (version.compareTo(Version.V4_0_0) < 0) { + return new Cassandra3SchemaQueries(channel, refreshFuture, config, logPrefix); + } else { + return new Cassandra4SchemaQueries(channel, refreshFuture, config, logPrefix); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaQueries.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaQueries.java new file mode 100644 index 00000000000..6ab89d190ca --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaQueries.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import java.util.concurrent.CompletionStage; + +/** + * Manages the queries to system tables during a schema refresh. + * + *

They are all asynchronous, and possibly paged. This class abstracts all the details and + * exposes a common result type. + * + *

Implementations must be thread-safe. + */ +public interface SchemaQueries { + + /** + * Launch the queries asynchronously, returning a future that will complete when they have all + * succeeded. + */ + CompletionStage execute(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaQueriesFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaQueriesFactory.java new file mode 100644 index 00000000000..94f1ae24d78 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaQueriesFactory.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import com.datastax.oss.driver.api.core.metadata.Metadata; +import java.util.concurrent.CompletableFuture; + +public interface SchemaQueriesFactory { + SchemaQueries newInstance(CompletableFuture refreshFuture); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaRows.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaRows.java new file mode 100644 index 00000000000..b8242517241 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaRows.java @@ -0,0 +1,64 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.DataTypeParser; +import com.datastax.oss.driver.shaded.guava.common.collect.Multimap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * The system rows returned by the queries for a schema refresh, categorized by keyspace/table where + * relevant. + * + *

Implementations must be thread-safe. + */ +public interface SchemaRows { + + List keyspaces(); + + List virtualKeyspaces(); + + Multimap tables(); + + Multimap virtualTables(); + + Multimap views(); + + Multimap types(); + + Multimap functions(); + + Multimap aggregates(); + + Map> columns(); + + Map> virtualColumns(); + + Map> indexes(); + + DataTypeParser dataTypeParser(); + + /** + * The future to complete when the schema refresh is complete (here just to be propagated further + * down the chain). + */ + CompletableFuture refreshFuture(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/refresh/SchemaRefresh.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/refresh/SchemaRefresh.java new file mode 100644 index 00000000000..0838b26e728 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/schema/refresh/SchemaRefresh.java @@ -0,0 +1,155 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.refresh; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultMetadata; +import com.datastax.oss.driver.internal.core.metadata.MetadataRefresh; +import com.datastax.oss.driver.internal.core.metadata.schema.events.AggregateChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.FunctionChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.KeyspaceChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.TableChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.TypeChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.ViewChangeEvent; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.Sets; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class SchemaRefresh implements MetadataRefresh { + + @VisibleForTesting public final Map newKeyspaces; + + public SchemaRefresh(Map newKeyspaces) { + this.newKeyspaces = newKeyspaces; + } + + @Override + public Result compute( + DefaultMetadata oldMetadata, boolean tokenMapEnabled, InternalDriverContext context) { + ImmutableList.Builder events = ImmutableList.builder(); + + Map oldKeyspaces = oldMetadata.getKeyspaces(); + for (CqlIdentifier removedKey : Sets.difference(oldKeyspaces.keySet(), newKeyspaces.keySet())) { + events.add(KeyspaceChangeEvent.dropped(oldKeyspaces.get(removedKey))); + } + for (Map.Entry entry : newKeyspaces.entrySet()) { + CqlIdentifier key = entry.getKey(); + computeEvents(oldKeyspaces.get(key), entry.getValue(), events); + } + + return new Result( + oldMetadata.withSchema(this.newKeyspaces, tokenMapEnabled, context), events.build()); + } + + private static boolean shallowEquals(KeyspaceMetadata keyspace1, KeyspaceMetadata keyspace2) { + return Objects.equals(keyspace1.getName(), keyspace2.getName()) + && keyspace1.isDurableWrites() == keyspace2.isDurableWrites() + && Objects.equals(keyspace1.getReplication(), keyspace2.getReplication()); + } + + /** + * Computes the exact set of events to emit when a keyspace has changed. + * + *

We can't simply emit {@link KeyspaceChangeEvent#updated(KeyspaceMetadata, KeyspaceMetadata)} + * because this method might be called as part of a full schema refresh, or a keyspace refresh + * initiated by coalesced child element refreshes. We need to traverse all children to check what + * has exactly changed. + */ + private void computeEvents( + KeyspaceMetadata oldKeyspace, + KeyspaceMetadata newKeyspace, + ImmutableList.Builder events) { + if (oldKeyspace == null) { + events.add(KeyspaceChangeEvent.created(newKeyspace)); + } else { + if (!shallowEquals(oldKeyspace, newKeyspace)) { + events.add(KeyspaceChangeEvent.updated(oldKeyspace, newKeyspace)); + } + computeChildEvents(oldKeyspace, newKeyspace, events); + } + } + + private void computeChildEvents( + KeyspaceMetadata oldKeyspace, + KeyspaceMetadata newKeyspace, + ImmutableList.Builder events) { + computeChildEvents( + oldKeyspace.getTables(), + newKeyspace.getTables(), + TableChangeEvent::dropped, + TableChangeEvent::created, + TableChangeEvent::updated, + events); + computeChildEvents( + oldKeyspace.getViews(), + newKeyspace.getViews(), + ViewChangeEvent::dropped, + ViewChangeEvent::created, + ViewChangeEvent::updated, + events); + computeChildEvents( + oldKeyspace.getUserDefinedTypes(), + newKeyspace.getUserDefinedTypes(), + TypeChangeEvent::dropped, + TypeChangeEvent::created, + TypeChangeEvent::updated, + events); + computeChildEvents( + oldKeyspace.getFunctions(), + newKeyspace.getFunctions(), + FunctionChangeEvent::dropped, + FunctionChangeEvent::created, + FunctionChangeEvent::updated, + events); + computeChildEvents( + oldKeyspace.getAggregates(), + newKeyspace.getAggregates(), + AggregateChangeEvent::dropped, + AggregateChangeEvent::created, + AggregateChangeEvent::updated, + events); + } + + private void computeChildEvents( + Map oldChildren, + Map newChildren, + Function newDroppedEvent, + Function newCreatedEvent, + BiFunction newUpdatedEvent, + ImmutableList.Builder events) { + for (K removedKey : Sets.difference(oldChildren.keySet(), newChildren.keySet())) { + events.add(newDroppedEvent.apply(oldChildren.get(removedKey))); + } + for (Map.Entry entry : newChildren.entrySet()) { + K key = entry.getKey(); + V newChild = entry.getValue(); + V oldChild = oldChildren.get(key); + if (oldChild == null) { + events.add(newCreatedEvent.apply(newChild)); + } else if (!oldChild.equals(newChild)) { + events.add(newUpdatedEvent.apply(oldChild, newChild)); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedToken.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedToken.java new file mode 100644 index 00000000000..dbe6f306fcf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedToken.java @@ -0,0 +1,82 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.primitives.UnsignedBytes; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import net.jcip.annotations.Immutable; + +/** A token generated by {@code ByteOrderedPartitioner}. */ +@Immutable +public class ByteOrderedToken implements Token { + + private final ByteBuffer value; + + public ByteOrderedToken(ByteBuffer value) { + this.value = stripTrailingZeroBytes(value); + } + + public ByteBuffer getValue() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof ByteOrderedToken) { + ByteOrderedToken that = (ByteOrderedToken) other; + return this.value.equals(that.getValue()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public int compareTo(@NonNull Token other) { + Preconditions.checkArgument( + other instanceof ByteOrderedToken, "Can only compare tokens of the same type"); + return UnsignedBytes.lexicographicalComparator() + .compare(Bytes.getArray(value), Bytes.getArray(((ByteOrderedToken) other).value)); + } + + @Override + public String toString() { + return "ByteOrderedToken(" + Bytes.toHexString(value) + ")"; + } + + private static ByteBuffer stripTrailingZeroBytes(ByteBuffer b) { + byte result[] = Bytes.getArray(b); + int zeroIndex = result.length; + for (int i = result.length - 1; i > 0; i--) { + if (result[i] == 0) { + zeroIndex = i; + } else { + break; + } + } + return ByteBuffer.wrap(result, 0, zeroIndex); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedTokenFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedTokenFactory.java new file mode 100644 index 00000000000..c53296f1878 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedTokenFactory.java @@ -0,0 +1,74 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class ByteOrderedTokenFactory implements TokenFactory { + + public static final String PARTITIONER_NAME = "org.apache.cassandra.dht.ByteOrderedPartitioner"; + + public static final ByteOrderedToken MIN_TOKEN = new ByteOrderedToken(ByteBuffer.allocate(0)); + + @Override + public String getPartitionerName() { + return PARTITIONER_NAME; + } + + @Override + public Token hash(ByteBuffer partitionKey) { + return new ByteOrderedToken(partitionKey); + } + + @Override + public Token parse(String tokenString) { + // This method must be able to parse the contents of system.peers.tokens, which do not have the + // "0x" prefix. On the other hand, OPPToken#toString has the "0x" because it should be usable in + // a CQL query, and it's nice to have fromString and toString symmetrical. So handle both cases: + if (!tokenString.startsWith("0x")) { + String prefix = (tokenString.length() % 2 == 0) ? "0x" : "0x0"; + tokenString = prefix + tokenString; + } + ByteBuffer value = Bytes.fromHexString(tokenString); + return new ByteOrderedToken(value); + } + + @Override + public String format(Token token) { + Preconditions.checkArgument( + token instanceof ByteOrderedToken, "Can only format ByteOrderedToken instances"); + return Bytes.toHexString(((ByteOrderedToken) token).getValue()); + } + + @Override + public Token minToken() { + return MIN_TOKEN; + } + + @Override + public TokenRange range(Token start, Token end) { + Preconditions.checkArgument( + start instanceof ByteOrderedToken && end instanceof ByteOrderedToken, + "Can only build ranges of ByteOrderedToken instances"); + return new ByteOrderedTokenRange(((ByteOrderedToken) start), ((ByteOrderedToken) end)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedTokenRange.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedTokenRange.java new file mode 100644 index 00000000000..7b3f2f92907 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedTokenRange.java @@ -0,0 +1,143 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.util.List; +import net.jcip.annotations.Immutable; + +@Immutable +public class ByteOrderedTokenRange extends TokenRangeBase { + + private static final BigInteger TWO = BigInteger.valueOf(2); + + public ByteOrderedTokenRange(ByteOrderedToken start, ByteOrderedToken end) { + super(start, end, ByteOrderedTokenFactory.MIN_TOKEN); + } + + @Override + protected TokenRange newTokenRange(Token start, Token end) { + return new ByteOrderedTokenRange(((ByteOrderedToken) start), ((ByteOrderedToken) end)); + } + + @Override + protected List split(Token rawStartToken, Token rawEndToken, int numberOfSplits) { + int tokenOrder = rawStartToken.compareTo(rawEndToken); + + // ]min,min] means the whole ring. However, since there is no "max token" with this partitioner, + // we can't come up with a magic end value that would cover the whole ring + if (tokenOrder == 0 && rawStartToken.equals(ByteOrderedTokenFactory.MIN_TOKEN)) { + throw new IllegalArgumentException("Cannot split whole ring with ordered partitioner"); + } + + ByteOrderedToken startToken = (ByteOrderedToken) rawStartToken; + ByteOrderedToken endToken = (ByteOrderedToken) rawEndToken; + + int significantBytes; + BigInteger start, end, range, ringEnd, ringLength; + BigInteger bigNumberOfSplits = BigInteger.valueOf(numberOfSplits); + if (tokenOrder < 0) { + // Since tokens are compared lexicographically, convert to integers using the largest length + // (ex: given 0x0A and 0x0BCD, switch to 0x0A00 and 0x0BCD) + significantBytes = Math.max(startToken.getValue().capacity(), endToken.getValue().capacity()); + + // If the number of splits does not fit in the difference between the two integers, use more + // bytes (ex: cannot fit 4 splits between 0x01 and 0x03, so switch to 0x0100 and 0x0300) + // At most 4 additional bytes will be needed, since numberOfSplits is an integer. + int addedBytes = 0; + while (true) { + start = toBigInteger(startToken.getValue(), significantBytes); + end = toBigInteger(endToken.getValue(), significantBytes); + range = end.subtract(start); + if (addedBytes == 4 || range.compareTo(bigNumberOfSplits) >= 0) { + break; + } + significantBytes += 1; + addedBytes += 1; + } + ringEnd = ringLength = null; // won't be used + } else { + // Same logic except that we wrap around the ring + significantBytes = Math.max(startToken.getValue().capacity(), endToken.getValue().capacity()); + int addedBytes = 0; + while (true) { + start = toBigInteger(startToken.getValue(), significantBytes); + end = toBigInteger(endToken.getValue(), significantBytes); + ringLength = TWO.pow(significantBytes * 8); + ringEnd = ringLength.subtract(BigInteger.ONE); + range = end.subtract(start).add(ringLength); + if (addedBytes == 4 || range.compareTo(bigNumberOfSplits) >= 0) { + break; + } + significantBytes += 1; + addedBytes += 1; + } + } + + List values = super.split(start, range, ringEnd, ringLength, numberOfSplits); + List tokens = Lists.newArrayListWithExpectedSize(values.size()); + for (BigInteger value : values) { + tokens.add(new ByteOrderedToken(toBytes(value, significantBytes))); + } + return tokens; + } + + // Convert a token's byte array to a number in order to perform computations. + // This depends on the number of "significant bytes" that we use to normalize all tokens to the + // same size. + // For example if the token is 0x01 but significantBytes is 2, the result is 8 (0x0100). + private BigInteger toBigInteger(ByteBuffer bb, int significantBytes) { + byte[] bytes = Bytes.getArray(bb); + byte[] target; + if (significantBytes != bytes.length) { + target = new byte[significantBytes]; + System.arraycopy(bytes, 0, target, 0, bytes.length); + } else { + target = bytes; + } + return new BigInteger(1, target); + } + + // Convert a numeric representation back to a byte array. + // Again, the number of significant bytes matters: if the input value is 1 but significantBytes is + // 2, the + // expected result is 0x0001 (a simple conversion would produce 0x01). + protected ByteBuffer toBytes(BigInteger value, int significantBytes) { + byte[] rawBytes = value.toByteArray(); + byte[] result; + if (rawBytes.length == significantBytes) { + result = rawBytes; + } else { + result = new byte[significantBytes]; + int start, length; + if (rawBytes[0] == 0) { + // that's a sign byte, ignore (it can cause rawBytes.length == significantBytes + 1) + start = 1; + length = rawBytes.length - 1; + } else { + start = 0; + length = rawBytes.length; + } + System.arraycopy(rawBytes, start, result, significantBytes - length, length); + } + return ByteBuffer.wrap(result); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultReplicationStrategyFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultReplicationStrategyFactory.java new file mode 100644 index 00000000000..603d1af07bf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultReplicationStrategyFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DefaultReplicationStrategyFactory implements ReplicationStrategyFactory { + + private final String logPrefix; + + public DefaultReplicationStrategyFactory(InternalDriverContext context) { + this.logPrefix = context.getSessionName(); + } + + @Override + public ReplicationStrategy newInstance(Map replicationConfig) { + String strategyClass = replicationConfig.get("class"); + Preconditions.checkNotNull( + strategyClass, "Missing replication strategy class in " + replicationConfig); + switch (strategyClass) { + case "org.apache.cassandra.locator.LocalStrategy": + return new LocalReplicationStrategy(); + case "org.apache.cassandra.locator.SimpleStrategy": + return new SimpleReplicationStrategy(replicationConfig); + case "org.apache.cassandra.locator.NetworkTopologyStrategy": + return new NetworkTopologyReplicationStrategy(replicationConfig, logPrefix); + default: + throw new IllegalArgumentException("Unsupported replication strategy: " + strategyClass); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultTokenFactoryRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultTokenFactoryRegistry.java new file mode 100644 index 00000000000..8717e4fb9d5 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultTokenFactoryRegistry.java @@ -0,0 +1,51 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class DefaultTokenFactoryRegistry implements TokenFactoryRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultTokenFactoryRegistry.class); + + private final String logPrefix; + + public DefaultTokenFactoryRegistry(InternalDriverContext context) { + this.logPrefix = context.getSessionName(); + } + + @Override + public TokenFactory tokenFactoryFor(String partitioner) { + if (Murmur3TokenFactory.PARTITIONER_NAME.equals(partitioner)) { + LOG.debug("[{}] Detected Murmur3 partitioner ({})", logPrefix, partitioner); + return new Murmur3TokenFactory(); + } else if (RandomTokenFactory.PARTITIONER_NAME.equals(partitioner)) { + LOG.debug("[{}] Detected random partitioner ({})", logPrefix, partitioner); + return new RandomTokenFactory(); + } else if (ByteOrderedTokenFactory.PARTITIONER_NAME.equals(partitioner)) { + LOG.debug("[{}] Detected byte ordered partitioner ({})", logPrefix, partitioner); + return new ByteOrderedTokenFactory(); + } else { + LOG.warn( + "[{}] Unsupported partitioner '{}', token map will be empty.", logPrefix, partitioner); + return null; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultTokenMap.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultTokenMap.java new file mode 100644 index 00000000000..0eeb399d672 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultTokenMap.java @@ -0,0 +1,301 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.TokenMap; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.util.RoutingKey; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSetMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.SetMultimap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import net.jcip.annotations.Immutable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Immutable +public class DefaultTokenMap implements TokenMap { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultTokenMap.class); + + public static DefaultTokenMap build( + @NonNull Collection nodes, + @NonNull Collection keyspaces, + @NonNull TokenFactory tokenFactory, + @NonNull ReplicationStrategyFactory replicationStrategyFactory, + @NonNull String logPrefix) { + + TokenToPrimaryAndRing tmp = buildTokenToPrimaryAndRing(nodes, tokenFactory); + Map tokenToPrimary = tmp.tokenToPrimary; + List ring = tmp.ring; + LOG.debug("[{}] Rebuilt ring ({} tokens)", logPrefix, ring.size()); + + Set tokenRanges = buildTokenRanges(ring, tokenFactory); + + ImmutableSetMultimap.Builder tokenRangesByPrimary = + ImmutableSetMultimap.builder(); + for (TokenRange range : tokenRanges) { + if (range.isFullRing()) { + // The full ring is always ]min, min], so getEnd() doesn't match the node's token + assert tokenToPrimary.size() == 1; + tokenRangesByPrimary.put(tokenToPrimary.values().iterator().next(), range); + } else { + tokenRangesByPrimary.put(tokenToPrimary.get(range.getEnd()), range); + } + } + + Map> replicationConfigs = + buildReplicationConfigs(keyspaces, logPrefix); + + ImmutableMap.Builder, KeyspaceTokenMap> keyspaceMapsBuilder = + ImmutableMap.builder(); + for (Map config : ImmutableSet.copyOf(replicationConfigs.values())) { + LOG.debug("[{}] Computing keyspace-level data for {}", logPrefix, config); + keyspaceMapsBuilder.put( + config, + KeyspaceTokenMap.build( + config, + tokenToPrimary, + ring, + tokenRanges, + tokenFactory, + replicationStrategyFactory, + logPrefix)); + } + return new DefaultTokenMap( + tokenFactory, + tokenRanges, + tokenRangesByPrimary.build(), + replicationConfigs, + keyspaceMapsBuilder.build(), + logPrefix); + } + + private final TokenFactory tokenFactory; + @VisibleForTesting final Set tokenRanges; + @VisibleForTesting final SetMultimap tokenRangesByPrimary; + @VisibleForTesting final Map> replicationConfigs; + @VisibleForTesting final Map, KeyspaceTokenMap> keyspaceMaps; + private final String logPrefix; + + private DefaultTokenMap( + TokenFactory tokenFactory, + Set tokenRanges, + SetMultimap tokenRangesByPrimary, + Map> replicationConfigs, + Map, KeyspaceTokenMap> keyspaceMaps, + String logPrefix) { + this.tokenFactory = tokenFactory; + this.tokenRanges = tokenRanges; + this.tokenRangesByPrimary = tokenRangesByPrimary; + this.replicationConfigs = replicationConfigs; + this.keyspaceMaps = keyspaceMaps; + this.logPrefix = logPrefix; + } + + public TokenFactory getTokenFactory() { + return tokenFactory; + } + + @NonNull + @Override + public Token parse(@NonNull String tokenString) { + return tokenFactory.parse(tokenString); + } + + @NonNull + @Override + public String format(@NonNull Token token) { + return tokenFactory.format(token); + } + + @NonNull + @Override + public Token newToken(@NonNull ByteBuffer... partitionKey) { + return tokenFactory.hash(RoutingKey.compose(partitionKey)); + } + + @NonNull + @Override + public TokenRange newTokenRange(@NonNull Token start, @NonNull Token end) { + return tokenFactory.range(start, end); + } + + @NonNull + @Override + public Set getTokenRanges() { + return tokenRanges; + } + + @NonNull + @Override + public Set getTokenRanges(@NonNull Node node) { + return tokenRangesByPrimary.get(node); + } + + @NonNull + @Override + public Set getTokenRanges(@NonNull CqlIdentifier keyspace, @NonNull Node replica) { + KeyspaceTokenMap keyspaceMap = getKeyspaceMap(keyspace); + return (keyspaceMap == null) ? Collections.emptySet() : keyspaceMap.getTokenRanges(replica); + } + + @NonNull + @Override + public Set getReplicas(@NonNull CqlIdentifier keyspace, @NonNull ByteBuffer partitionKey) { + KeyspaceTokenMap keyspaceMap = getKeyspaceMap(keyspace); + return (keyspaceMap == null) ? Collections.emptySet() : keyspaceMap.getReplicas(partitionKey); + } + + @NonNull + @Override + public Set getReplicas(@NonNull CqlIdentifier keyspace, @NonNull Token token) { + KeyspaceTokenMap keyspaceMap = getKeyspaceMap(keyspace); + return (keyspaceMap == null) ? Collections.emptySet() : keyspaceMap.getReplicas(token); + } + + @NonNull + @Override + public String getPartitionerName() { + return tokenFactory.getPartitionerName(); + } + + private KeyspaceTokenMap getKeyspaceMap(CqlIdentifier keyspace) { + Map config = replicationConfigs.get(keyspace); + return (config == null) ? null : keyspaceMaps.get(config); + } + + /** Called when only the schema has changed. */ + public DefaultTokenMap refresh( + @NonNull Collection nodes, + @NonNull Collection keyspaces, + @NonNull ReplicationStrategyFactory replicationStrategyFactory) { + + Map> newReplicationConfigs = + buildReplicationConfigs(keyspaces, logPrefix); + if (newReplicationConfigs.equals(replicationConfigs)) { + LOG.debug("[{}] Schema changes do not impact the token map, no refresh needed", logPrefix); + return this; + } + ImmutableMap.Builder, KeyspaceTokenMap> newKeyspaceMapsBuilder = + ImmutableMap.builder(); + + // Will only be built if needed: + Map tokenToPrimary = null; + List ring = null; + + for (Map config : ImmutableSet.copyOf(newReplicationConfigs.values())) { + KeyspaceTokenMap oldKeyspaceMap = keyspaceMaps.get(config); + if (oldKeyspaceMap != null) { + LOG.debug("[{}] Reusing existing keyspace-level data for {}", logPrefix, config); + newKeyspaceMapsBuilder.put(config, oldKeyspaceMap); + } else { + LOG.debug("[{}] Computing new keyspace-level data for {}", logPrefix, config); + if (tokenToPrimary == null) { + TokenToPrimaryAndRing tmp = buildTokenToPrimaryAndRing(nodes, tokenFactory); + tokenToPrimary = tmp.tokenToPrimary; + ring = tmp.ring; + } + newKeyspaceMapsBuilder.put( + config, + KeyspaceTokenMap.build( + config, + tokenToPrimary, + ring, + tokenRanges, + tokenFactory, + replicationStrategyFactory, + logPrefix)); + } + } + return new DefaultTokenMap( + tokenFactory, + tokenRanges, + tokenRangesByPrimary, + newReplicationConfigs, + newKeyspaceMapsBuilder.build(), + logPrefix); + } + + private static TokenToPrimaryAndRing buildTokenToPrimaryAndRing( + Collection nodes, TokenFactory tokenFactory) { + ImmutableMap.Builder tokenToPrimaryBuilder = ImmutableMap.builder(); + SortedSet sortedTokens = new TreeSet<>(); + for (Node node : nodes) { + for (String tokenString : ((DefaultNode) node).getRawTokens()) { + Token token = tokenFactory.parse(tokenString); + sortedTokens.add(token); + tokenToPrimaryBuilder.put(token, node); + } + } + return new TokenToPrimaryAndRing( + tokenToPrimaryBuilder.build(), ImmutableList.copyOf(sortedTokens)); + } + + static class TokenToPrimaryAndRing { + final Map tokenToPrimary; + final List ring; + + private TokenToPrimaryAndRing(Map tokenToPrimary, List ring) { + this.tokenToPrimary = tokenToPrimary; + this.ring = ring; + } + } + + private static Map> buildReplicationConfigs( + Collection keyspaces, String logPrefix) { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (KeyspaceMetadata keyspace : keyspaces) { + if (!keyspace.isVirtual()) { + builder.put(keyspace.getName(), keyspace.getReplication()); + } + } + ImmutableMap> result = builder.build(); + LOG.debug("[{}] Computing keyspace-level data for {}", logPrefix, result); + return result; + } + + private static Set buildTokenRanges(List ring, TokenFactory factory) { + ImmutableSet.Builder builder = ImmutableSet.builder(); + // JAVA-684: if there is only one token, return the full ring (]minToken, minToken]) + if (ring.size() == 1) { + builder.add(factory.range(factory.minToken(), factory.minToken())); + } else { + for (int i = 0; i < ring.size(); i++) { + Token start = ring.get(i); + Token end = ring.get((i + 1) % ring.size()); + builder.add(factory.range(start, end)); + } + } + return builder.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/KeyspaceTokenMap.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/KeyspaceTokenMap.java new file mode 100644 index 00000000000..d5f6937dd93 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/KeyspaceTokenMap.java @@ -0,0 +1,131 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.internal.core.util.NanoTime; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSetMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.SetMultimap; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.jcip.annotations.Immutable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The token data for a given replication configuration. It's shared by all keyspaces that use that + * configuration. + */ +@Immutable +class KeyspaceTokenMap { + + private static final Logger LOG = LoggerFactory.getLogger(KeyspaceTokenMap.class); + + static KeyspaceTokenMap build( + Map replicationConfig, + Map tokenToPrimary, + List ring, + Set tokenRanges, + TokenFactory tokenFactory, + ReplicationStrategyFactory replicationStrategyFactory, + String logPrefix) { + + long start = System.nanoTime(); + try { + ReplicationStrategy strategy = replicationStrategyFactory.newInstance(replicationConfig); + + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + SetMultimap tokenRangesByNode; + if (ring.size() == 1) { + // We forced the single range to ]minToken,minToken], make sure to use that instead of + // relying + // on the node's token + ImmutableSetMultimap.Builder builder = ImmutableSetMultimap.builder(); + for (Node node : tokenToPrimary.values()) { + builder.putAll(node, tokenRanges); + } + tokenRangesByNode = builder.build(); + } else { + tokenRangesByNode = buildTokenRangesByNode(tokenRanges, replicasByToken); + } + return new KeyspaceTokenMap(ring, tokenRangesByNode, replicasByToken, tokenFactory); + } finally { + LOG.debug( + "[{}] Computing keyspace-level data for {} took {}", + logPrefix, + replicationConfig, + NanoTime.formatTimeSince(start)); + } + } + + private final List ring; + private final SetMultimap tokenRangesByNode; + private final SetMultimap replicasByToken; + private final TokenFactory tokenFactory; + + private KeyspaceTokenMap( + List ring, + SetMultimap tokenRangesByNode, + SetMultimap replicasByToken, + TokenFactory tokenFactory) { + this.ring = ring; + this.tokenRangesByNode = tokenRangesByNode; + this.replicasByToken = replicasByToken; + this.tokenFactory = tokenFactory; + } + + Set getTokenRanges(Node replica) { + return tokenRangesByNode.get(replica); + } + + Set getReplicas(ByteBuffer partitionKey) { + return getReplicas(tokenFactory.hash(partitionKey)); + } + + Set getReplicas(Token token) { + // If the token happens to be one of the "primary" tokens, get result directly + Set nodes = replicasByToken.get(token); + if (!nodes.isEmpty()) { + return nodes; + } + // Otherwise, find the closest "primary" token on the ring + int i = Collections.binarySearch(ring, token); + if (i < 0) { + i = -i - 1; + if (i >= ring.size()) { + i = 0; + } + } + return replicasByToken.get(ring.get(i)); + } + + private static SetMultimap buildTokenRangesByNode( + Set tokenRanges, SetMultimap replicasByToken) { + ImmutableSetMultimap.Builder result = ImmutableSetMultimap.builder(); + for (TokenRange range : tokenRanges) { + for (Node node : replicasByToken.get(range.getEnd())) { + result.put(node, range); + } + } + return result.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/LocalReplicationStrategy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/LocalReplicationStrategy.java new file mode 100644 index 00000000000..6e1395fbf2d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/LocalReplicationStrategy.java @@ -0,0 +1,38 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSetMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.SetMultimap; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +class LocalReplicationStrategy implements ReplicationStrategy { + + @Override + public SetMultimap computeReplicasByToken( + Map tokenToPrimary, List ring) { + ImmutableSetMultimap.Builder result = ImmutableSetMultimap.builder(); + for (Map.Entry entry : tokenToPrimary.entrySet()) { + result.put(entry.getKey(), entry.getValue()); + } + return result.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3Token.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3Token.java new file mode 100644 index 00000000000..fdf67ad2461 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3Token.java @@ -0,0 +1,67 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.primitives.Longs; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +/** A token generated by {@code Murmur3Partitioner}. */ +@Immutable +public class Murmur3Token implements Token { + + private final long value; + + public Murmur3Token(long value) { + this.value = value; + } + + public long getValue() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof Murmur3Token) { + Murmur3Token that = (Murmur3Token) other; + return this.value == that.value; + } else { + return false; + } + } + + @Override + public int hashCode() { + return (int) (value ^ (value >>> 32)); + } + + @Override + public int compareTo(@NonNull Token other) { + Preconditions.checkArgument( + other instanceof Murmur3Token, "Can only compare tokens of the same type"); + Murmur3Token that = (Murmur3Token) other; + return Longs.compare(this.value, that.value); + } + + @Override + public String toString() { + return "Murmur3Token(" + value + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3TokenFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3TokenFactory.java new file mode 100644 index 00000000000..252c59f671b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3TokenFactory.java @@ -0,0 +1,212 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class Murmur3TokenFactory implements TokenFactory { + + public static final String PARTITIONER_NAME = "org.apache.cassandra.dht.Murmur3Partitioner"; + + public static final Murmur3Token MIN_TOKEN = new Murmur3Token(Long.MIN_VALUE); + public static final Murmur3Token MAX_TOKEN = new Murmur3Token(Long.MAX_VALUE); + + @Override + public String getPartitionerName() { + return PARTITIONER_NAME; + } + + @Override + public Token hash(ByteBuffer partitionKey) { + long v = murmur(partitionKey); + return new Murmur3Token(v == Long.MIN_VALUE ? Long.MAX_VALUE : v); + } + + @Override + public Token parse(String tokenString) { + return new Murmur3Token(Long.parseLong(tokenString)); + } + + @Override + public String format(Token token) { + Preconditions.checkArgument( + token instanceof Murmur3Token, "Can only format Murmur3Token instances"); + return Long.toString(((Murmur3Token) token).getValue()); + } + + @Override + public Token minToken() { + return MIN_TOKEN; + } + + @Override + public TokenRange range(Token start, Token end) { + Preconditions.checkArgument( + start instanceof Murmur3Token && end instanceof Murmur3Token, + "Can only build ranges of Murmur3Token instances"); + return new Murmur3TokenRange((Murmur3Token) start, (Murmur3Token) end); + } + + // This is an adapted version of the MurmurHash.hash3_x64_128 from Cassandra used + // for M3P. Compared to that methods, there's a few inlining of arguments and we + // only return the first 64-bits of the result since that's all M3P uses. + private long murmur(ByteBuffer data) { + int offset = data.position(); + int length = data.remaining(); + + int nblocks = length >> 4; // Process as 128-bit blocks. + + long h1 = 0; + long h2 = 0; + + long c1 = 0x87c37b91114253d5L; + long c2 = 0x4cf5ad432745937fL; + + // ---------- + // body + + for (int i = 0; i < nblocks; i++) { + long k1 = getblock(data, offset, i * 2); + long k2 = getblock(data, offset, i * 2 + 1); + + k1 *= c1; + k1 = rotl64(k1, 31); + k1 *= c2; + h1 ^= k1; + h1 = rotl64(h1, 27); + h1 += h2; + h1 = h1 * 5 + 0x52dce729; + k2 *= c2; + k2 = rotl64(k2, 33); + k2 *= c1; + h2 ^= k2; + h2 = rotl64(h2, 31); + h2 += h1; + h2 = h2 * 5 + 0x38495ab5; + } + + // ---------- + // tail + + // Advance offset to the unprocessed tail of the data. + offset += nblocks * 16; + + long k1 = 0; + long k2 = 0; + + switch (length & 15) { + case 15: + k2 ^= ((long) data.get(offset + 14)) << 48; + // fall through + case 14: + k2 ^= ((long) data.get(offset + 13)) << 40; + // fall through + case 13: + k2 ^= ((long) data.get(offset + 12)) << 32; + // fall through + case 12: + k2 ^= ((long) data.get(offset + 11)) << 24; + // fall through + case 11: + k2 ^= ((long) data.get(offset + 10)) << 16; + // fall through + case 10: + k2 ^= ((long) data.get(offset + 9)) << 8; + // fall through + case 9: + k2 ^= ((long) data.get(offset + 8)); + k2 *= c2; + k2 = rotl64(k2, 33); + k2 *= c1; + h2 ^= k2; + // fall through + case 8: + k1 ^= ((long) data.get(offset + 7)) << 56; + // fall through + case 7: + k1 ^= ((long) data.get(offset + 6)) << 48; + // fall through + case 6: + k1 ^= ((long) data.get(offset + 5)) << 40; + // fall through + case 5: + k1 ^= ((long) data.get(offset + 4)) << 32; + // fall through + case 4: + k1 ^= ((long) data.get(offset + 3)) << 24; + // fall through + case 3: + k1 ^= ((long) data.get(offset + 2)) << 16; + // fall through + case 2: + k1 ^= ((long) data.get(offset + 1)) << 8; + // fall through + case 1: + k1 ^= ((long) data.get(offset)); + k1 *= c1; + k1 = rotl64(k1, 31); + k1 *= c2; + h1 ^= k1; + } + + // ---------- + // finalization + + h1 ^= length; + h2 ^= length; + + h1 += h2; + h2 += h1; + + h1 = fmix(h1); + h2 = fmix(h2); + + h1 += h2; + + return h1; + } + + private long getblock(ByteBuffer key, int offset, int index) { + int i_8 = index << 3; + int blockOffset = offset + i_8; + return ((long) key.get(blockOffset) & 0xff) + + (((long) key.get(blockOffset + 1) & 0xff) << 8) + + (((long) key.get(blockOffset + 2) & 0xff) << 16) + + (((long) key.get(blockOffset + 3) & 0xff) << 24) + + (((long) key.get(blockOffset + 4) & 0xff) << 32) + + (((long) key.get(blockOffset + 5) & 0xff) << 40) + + (((long) key.get(blockOffset + 6) & 0xff) << 48) + + (((long) key.get(blockOffset + 7) & 0xff) << 56); + } + + private long rotl64(long v, int n) { + return ((v << n) | (v >>> (64 - n))); + } + + private long fmix(long k) { + k ^= k >>> 33; + k *= 0xff51afd7ed558ccdL; + k ^= k >>> 33; + k *= 0xc4ceb9fe1a85ec53L; + k ^= k >>> 33; + return k; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3TokenRange.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3TokenRange.java new file mode 100644 index 00000000000..5586204375f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3TokenRange.java @@ -0,0 +1,63 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.math.BigInteger; +import java.util.List; +import net.jcip.annotations.Immutable; + +@Immutable +public class Murmur3TokenRange extends TokenRangeBase { + + private static final BigInteger RING_END = BigInteger.valueOf(Long.MAX_VALUE); + private static final BigInteger RING_LENGTH = + RING_END.subtract(BigInteger.valueOf(Long.MIN_VALUE)); + + public Murmur3TokenRange(Murmur3Token start, Murmur3Token end) { + super(start, end, Murmur3TokenFactory.MIN_TOKEN); + } + + @Override + protected TokenRange newTokenRange(Token start, Token end) { + return new Murmur3TokenRange((Murmur3Token) start, (Murmur3Token) end); + } + + @Override + protected List split(Token startToken, Token endToken, int numberOfSplits) { + // edge case: ]min, min] means the whole ring + if (startToken.equals(endToken) && startToken.equals(Murmur3TokenFactory.MIN_TOKEN)) { + endToken = Murmur3TokenFactory.MAX_TOKEN; + } + + BigInteger start = BigInteger.valueOf(((Murmur3Token) startToken).getValue()); + BigInteger end = BigInteger.valueOf(((Murmur3Token) endToken).getValue()); + + BigInteger range = end.subtract(start); + if (range.compareTo(BigInteger.ZERO) < 0) { + range = range.add(RING_LENGTH); + } + + List values = super.split(start, range, RING_END, RING_LENGTH, numberOfSplits); + List tokens = Lists.newArrayListWithExpectedSize(values.size()); + for (BigInteger value : values) { + tokens.add(new Murmur3Token(value.longValue())); + } + return tokens; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/NetworkTopologyReplicationStrategy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/NetworkTopologyReplicationStrategy.java new file mode 100644 index 00000000000..8315ccac2ab --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/NetworkTopologyReplicationStrategy.java @@ -0,0 +1,170 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSetMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.Maps; +import com.datastax.oss.driver.shaded.guava.common.collect.SetMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.Sets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +class NetworkTopologyReplicationStrategy implements ReplicationStrategy { + + private static final Logger LOG = + LoggerFactory.getLogger(NetworkTopologyReplicationStrategy.class); + + private final Map replicationConfig; + private final Map replicationFactors; + private final String logPrefix; + + NetworkTopologyReplicationStrategy(Map replicationConfig, String logPrefix) { + this.replicationConfig = replicationConfig; + ImmutableMap.Builder factorsBuilder = ImmutableMap.builder(); + for (Map.Entry entry : replicationConfig.entrySet()) { + if (!entry.getKey().equals("class")) { + factorsBuilder.put(entry.getKey(), Integer.parseInt(entry.getValue())); + } + } + this.replicationFactors = factorsBuilder.build(); + this.logPrefix = logPrefix; + } + + @Override + public SetMultimap computeReplicasByToken( + Map tokenToPrimary, List ring) { + + // This is essentially a copy of org.apache.cassandra.locator.NetworkTopologyStrategy + ImmutableSetMultimap.Builder result = ImmutableSetMultimap.builder(); + Map> racks = getRacksInDcs(tokenToPrimary.values()); + Map dcNodeCount = Maps.newHashMapWithExpectedSize(replicationFactors.size()); + Set warnedDcs = Sets.newHashSetWithExpectedSize(replicationFactors.size()); + // find maximum number of nodes in each DC + for (Node node : Sets.newHashSet(tokenToPrimary.values())) { + String dc = node.getDatacenter(); + dcNodeCount.putIfAbsent(dc, 0); + dcNodeCount.put(dc, dcNodeCount.get(dc) + 1); + } + for (int i = 0; i < ring.size(); i++) { + Map> allDcReplicas = new HashMap<>(); + Map> seenRacks = new HashMap<>(); + Map> skippedDcEndpoints = new HashMap<>(); + for (String dc : replicationFactors.keySet()) { + allDcReplicas.put(dc, new HashSet<>()); + seenRacks.put(dc, new HashSet<>()); + skippedDcEndpoints.put(dc, new LinkedHashSet<>()); // preserve order + } + + // Preserve order - primary replica will be first + Set replicas = new LinkedHashSet<>(); + for (int j = 0; j < ring.size() && !allDone(allDcReplicas, dcNodeCount); j++) { + Node h = tokenToPrimary.get(getTokenWrapping(i + j, ring)); + String dc = h.getDatacenter(); + if (dc == null || !allDcReplicas.containsKey(dc)) { + continue; + } + Integer rf = replicationFactors.get(dc); + Set dcReplicas = allDcReplicas.get(dc); + if (rf == null || dcReplicas.size() >= rf) { + continue; + } + String rack = h.getRack(); + // Check if we already visited all racks in dc + if (rack == null || seenRacks.get(dc).size() == racks.get(dc).size()) { + replicas.add(h); + dcReplicas.add(h); + } else { + // Is this a new rack? + if (seenRacks.get(dc).contains(rack)) { + skippedDcEndpoints.get(dc).add(h); + } else { + replicas.add(h); + dcReplicas.add(h); + seenRacks.get(dc).add(rack); + // If we've run out of distinct racks, add the nodes skipped so far + if (seenRacks.get(dc).size() == racks.get(dc).size()) { + Iterator skippedIt = skippedDcEndpoints.get(dc).iterator(); + while (skippedIt.hasNext() && dcReplicas.size() < rf) { + Node nextSkipped = skippedIt.next(); + replicas.add(nextSkipped); + dcReplicas.add(nextSkipped); + } + } + } + } + } + // If we haven't found enough replicas after a whole trip around the ring, this probably + // means that the replication factors are broken. + // Warn the user because that leads to quadratic performance of this method (JAVA-702). + for (Map.Entry> entry : allDcReplicas.entrySet()) { + String dcName = entry.getKey(); + int expectedFactor = replicationFactors.get(dcName); + int achievedFactor = entry.getValue().size(); + if (achievedFactor < expectedFactor && !warnedDcs.contains(dcName)) { + LOG.warn( + "[{}] Error while computing token map for replication settings {}: " + + "could not achieve replication factor {} for datacenter {} (found only {} replicas).", + logPrefix, + replicationConfig, + expectedFactor, + dcName, + achievedFactor); + // only warn once per DC + warnedDcs.add(dcName); + } + } + + result.putAll(ring.get(i), replicas); + } + return result.build(); + } + + private boolean allDone(Map> map, Map dcNodeCount) { + for (Map.Entry> entry : map.entrySet()) { + String dc = entry.getKey(); + int dcCount = (dcNodeCount.get(dc) == null) ? 0 : dcNodeCount.get(dc); + if (entry.getValue().size() < Math.min(replicationFactors.get(dc), dcCount)) { + return false; + } + } + return true; + } + + private Map> getRacksInDcs(Iterable nodes) { + Map> result = new HashMap<>(); + for (Node node : nodes) { + Set racks = result.computeIfAbsent(node.getDatacenter(), k -> new HashSet<>()); + racks.add(node.getRack()); + } + return result; + } + + private static Token getTokenWrapping(int i, List ring) { + return ring.get(i % ring.size()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/RandomToken.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/RandomToken.java new file mode 100644 index 00000000000..b9f6f2d2fb1 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/RandomToken.java @@ -0,0 +1,67 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigInteger; +import net.jcip.annotations.Immutable; + +/** A token generated by {@code RandomPartitioner}. */ +@Immutable +public class RandomToken implements Token { + + private final BigInteger value; + + public RandomToken(BigInteger value) { + this.value = value; + } + + public BigInteger getValue() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof RandomToken) { + RandomToken that = (RandomToken) other; + return this.value.equals(that.value); + } else { + return false; + } + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public int compareTo(@NonNull Token other) { + Preconditions.checkArgument( + other instanceof RandomToken, "Can only compare tokens of the same type"); + RandomToken that = (RandomToken) other; + return this.value.compareTo(that.getValue()); + } + + @Override + public String toString() { + return "RandomToken(" + value + ")"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/RandomTokenFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/RandomTokenFactory.java new file mode 100644 index 00000000000..b4e9fdaa28a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/RandomTokenFactory.java @@ -0,0 +1,110 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class RandomTokenFactory implements TokenFactory { + + public static final String PARTITIONER_NAME = "org.apache.cassandra.dht.RandomPartitioner"; + + private static final BigInteger MIN_VALUE = BigInteger.ONE.negate(); + static final BigInteger MAX_VALUE = BigInteger.valueOf(2).pow(127); + public static final RandomToken MIN_TOKEN = new RandomToken(MIN_VALUE); + public static final RandomToken MAX_TOKEN = new RandomToken(MAX_VALUE); + + private final MessageDigest prototype; + private final boolean supportsClone; + + public RandomTokenFactory() { + prototype = createMessageDigest(); + boolean supportsClone; + try { + prototype.clone(); + supportsClone = true; + } catch (CloneNotSupportedException e) { + supportsClone = false; + } + this.supportsClone = supportsClone; + } + + @Override + public String getPartitionerName() { + return PARTITIONER_NAME; + } + + @Override + public Token hash(ByteBuffer partitionKey) { + return new RandomToken(md5(partitionKey)); + } + + @Override + public Token parse(String tokenString) { + return new RandomToken(new BigInteger(tokenString)); + } + + @Override + public String format(Token token) { + Preconditions.checkArgument( + token instanceof RandomToken, "Can only format RandomToken instances"); + return ((RandomToken) token).getValue().toString(); + } + + @Override + public Token minToken() { + return MIN_TOKEN; + } + + @Override + public TokenRange range(Token start, Token end) { + Preconditions.checkArgument( + start instanceof RandomToken && end instanceof RandomToken, + "Can only build ranges of RandomToken instances"); + return new RandomTokenRange((RandomToken) start, (RandomToken) end); + } + + private static MessageDigest createMessageDigest() { + try { + return MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 doesn't seem to be available on this JVM", e); + } + } + + private BigInteger md5(ByteBuffer data) { + MessageDigest digest = newMessageDigest(); + digest.update(data.duplicate()); + return new BigInteger(digest.digest()).abs(); + } + + private MessageDigest newMessageDigest() { + if (supportsClone) { + try { + return (MessageDigest) prototype.clone(); + } catch (CloneNotSupportedException ignored) { + } + } + return createMessageDigest(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/RandomTokenRange.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/RandomTokenRange.java new file mode 100644 index 00000000000..74dc36265de --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/RandomTokenRange.java @@ -0,0 +1,63 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import static com.datastax.oss.driver.internal.core.metadata.token.RandomTokenFactory.MAX_VALUE; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.math.BigInteger; +import java.util.List; +import net.jcip.annotations.Immutable; + +@Immutable +public class RandomTokenRange extends TokenRangeBase { + + private static final BigInteger RING_LENGTH = MAX_VALUE.add(BigInteger.ONE); + + public RandomTokenRange(RandomToken start, RandomToken end) { + super(start, end, RandomTokenFactory.MIN_TOKEN); + } + + @Override + protected TokenRange newTokenRange(Token start, Token end) { + return new RandomTokenRange(((RandomToken) start), ((RandomToken) end)); + } + + @Override + protected List split(Token startToken, Token endToken, int numberOfSplits) { + // edge case: ]min, min] means the whole ring + if (startToken.equals(endToken) && startToken.equals(RandomTokenFactory.MIN_TOKEN)) { + endToken = RandomTokenFactory.MAX_TOKEN; + } + + BigInteger start = ((RandomToken) startToken).getValue(); + BigInteger end = ((RandomToken) endToken).getValue(); + + BigInteger range = end.subtract(start); + if (range.compareTo(BigInteger.ZERO) < 0) { + range = range.add(RING_LENGTH); + } + + List values = super.split(start, range, MAX_VALUE, RING_LENGTH, numberOfSplits); + List tokens = Lists.newArrayListWithExpectedSize(values.size()); + for (BigInteger value : values) { + tokens.add(new RandomToken(value)); + } + return tokens; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ReplicationStrategy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ReplicationStrategy.java new file mode 100644 index 00000000000..1049c66c81b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ReplicationStrategy.java @@ -0,0 +1,27 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.shaded.guava.common.collect.SetMultimap; +import java.util.List; +import java.util.Map; + +public interface ReplicationStrategy { + SetMultimap computeReplicasByToken( + Map tokenToPrimary, List ring); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ReplicationStrategyFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ReplicationStrategyFactory.java new file mode 100644 index 00000000000..2b7bff0316c --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/ReplicationStrategyFactory.java @@ -0,0 +1,22 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import java.util.Map; + +public interface ReplicationStrategyFactory { + ReplicationStrategy newInstance(Map replicationConfig); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/SimpleReplicationStrategy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/SimpleReplicationStrategy.java new file mode 100644 index 00000000000..4e02dee46bd --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/SimpleReplicationStrategy.java @@ -0,0 +1,71 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSetMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.SetMultimap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +class SimpleReplicationStrategy implements ReplicationStrategy { + + private final int replicationFactor; + + SimpleReplicationStrategy(Map replicationConfig) { + this(extractReplicationFactor(replicationConfig)); + } + + @VisibleForTesting + SimpleReplicationStrategy(int replicationFactor) { + this.replicationFactor = replicationFactor; + } + + @Override + public SetMultimap computeReplicasByToken( + Map tokenToPrimary, List ring) { + + int rf = Math.min(replicationFactor, ring.size()); + + ImmutableSetMultimap.Builder result = ImmutableSetMultimap.builder(); + for (int i = 0; i < ring.size(); i++) { + // Consecutive sections of the ring can be assigned to the same node + Set replicas = new LinkedHashSet<>(); + for (int j = 0; j < ring.size() && replicas.size() < rf; j++) { + replicas.add(tokenToPrimary.get(getTokenWrapping(i + j, ring))); + } + result.putAll(ring.get(i), replicas); + } + return result.build(); + } + + private static Token getTokenWrapping(int i, List ring) { + return ring.get(i % ring.size()); + } + + private static int extractReplicationFactor(Map replicationConfig) { + String factorString = replicationConfig.get("replication_factor"); + Preconditions.checkNotNull(factorString, "Missing replication factor in " + replicationConfig); + return Integer.parseInt(factorString); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/TokenFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/TokenFactory.java new file mode 100644 index 00000000000..e727a36cbb2 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/TokenFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import java.nio.ByteBuffer; + +/** Manages token instances for a partitioner implementation. */ +public interface TokenFactory { + + String getPartitionerName(); + + Token hash(ByteBuffer partitionKey); + + Token parse(String tokenString); + + String format(Token token); + + /** + * The minimum token is a special value that no key ever hashes to, it's used both as lower and + * upper bound. + */ + Token minToken(); + + TokenRange range(Token start, Token end); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/TokenFactoryRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/TokenFactoryRegistry.java new file mode 100644 index 00000000000..740d14ce924 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/TokenFactoryRegistry.java @@ -0,0 +1,21 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +/** A thin layer of indirection to make token factories pluggable. */ +public interface TokenFactoryRegistry { + TokenFactory tokenFactoryFor(String partitioner); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/TokenRangeBase.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/TokenRangeBase.java new file mode 100644 index 00000000000..4c3ffe21b50 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metadata/token/TokenRangeBase.java @@ -0,0 +1,295 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public abstract class TokenRangeBase implements TokenRange { + + private final Token start; + private final Token end; + private final Token minToken; + + protected TokenRangeBase(Token start, Token end, Token minToken) { + this.start = start; + this.end = end; + this.minToken = minToken; + } + + @NonNull + @Override + public Token getStart() { + return start; + } + + @NonNull + @Override + public Token getEnd() { + return end; + } + + @NonNull + @Override + public List splitEvenly(int numberOfSplits) { + if (numberOfSplits < 1) + throw new IllegalArgumentException( + String.format("numberOfSplits (%d) must be greater than 0.", numberOfSplits)); + if (isEmpty()) { + throw new IllegalArgumentException("Can't split empty range " + this); + } + + List tokenRanges = new ArrayList<>(); + List splitPoints = split(start, end, numberOfSplits); + Token splitStart = start; + for (Token splitEnd : splitPoints) { + tokenRanges.add(newTokenRange(splitStart, splitEnd)); + splitStart = splitEnd; + } + tokenRanges.add(newTokenRange(splitStart, end)); + return tokenRanges; + } + + protected abstract List split(Token start, Token end, int numberOfSplits); + + /** This is used by {@link #split(Token, Token, int)} implementations. */ + protected List split( + BigInteger start, + BigInteger range, + BigInteger ringEnd, + BigInteger ringLength, + int numberOfSplits) { + BigInteger[] tmp = range.divideAndRemainder(BigInteger.valueOf(numberOfSplits)); + BigInteger divider = tmp[0]; + int remainder = tmp[1].intValue(); + + List results = Lists.newArrayListWithExpectedSize(numberOfSplits - 1); + BigInteger current = start; + BigInteger dividerPlusOne = + (remainder == 0) + ? null // won't be used + : divider.add(BigInteger.ONE); + + for (int i = 1; i < numberOfSplits; i++) { + current = current.add(remainder-- > 0 ? dividerPlusOne : divider); + if (ringEnd != null && current.compareTo(ringEnd) > 0) current = current.subtract(ringLength); + results.add(current); + } + return results; + } + + protected abstract TokenRange newTokenRange(Token start, Token end); + + @Override + public boolean isEmpty() { + return start.equals(end) && !start.equals(minToken); + } + + @Override + public boolean isWrappedAround() { + return start.compareTo(end) > 0 && !end.equals(minToken); + } + + @Override + public boolean isFullRing() { + return start.equals(minToken) && end.equals(minToken); + } + + @NonNull + @Override + public List unwrap() { + if (isWrappedAround()) { + return ImmutableList.of(newTokenRange(start, minToken), newTokenRange(minToken, end)); + } else { + return ImmutableList.of(this); + } + } + + @Override + public boolean intersects(@NonNull TokenRange that) { + // Empty ranges never intersect any other range + if (this.isEmpty() || that.isEmpty()) { + return false; + } + + return contains(this, that.getStart(), true) + || contains(this, that.getEnd(), false) + || contains(that, this.start, true) + || contains(that, this.end, false); + } + + @NonNull + @Override + public List intersectWith(@NonNull TokenRange that) { + if (!this.intersects(that)) { + throw new IllegalArgumentException( + "The two ranges do not intersect, use intersects() before calling this method"); + } + + List intersected = Lists.newArrayList(); + + // Compare the unwrapped ranges to one another. + List unwrappedForThis = this.unwrap(); + List unwrappedForThat = that.unwrap(); + for (TokenRange t1 : unwrappedForThis) { + for (TokenRange t2 : unwrappedForThat) { + if (t1.intersects(t2)) { + intersected.add( + newTokenRange( + (contains(t1, t2.getStart(), true)) ? t2.getStart() : t1.getStart(), + (contains(t1, t2.getEnd(), false)) ? t2.getEnd() : t1.getEnd())); + } + } + } + + // If two intersecting ranges were produced, merge them if they are adjacent. + // This could happen in the case that two wrapped ranges intersected. + if (intersected.size() == 2) { + TokenRange t1 = intersected.get(0); + TokenRange t2 = intersected.get(1); + if (t1.getEnd().equals(t2.getStart()) || t2.getEnd().equals(t1.getStart())) { + return ImmutableList.of(t1.mergeWith(t2)); + } + } + + return intersected; + } + + @Override + public boolean contains(@NonNull Token token) { + return contains(this, token, false); + } + + // isStart handles the case where the token is the start of another range, for example: + // * ]1,2] contains 2, but it does not contain the start of ]2,3] + // * ]1,2] does not contain 1, but it contains the start of ]1,3] + @VisibleForTesting + boolean contains(TokenRange range, Token token, boolean isStart) { + if (range.isEmpty()) { + return false; + } + if (range.getEnd().equals(minToken)) { + if (range.getStart().equals(minToken)) { // ]min, min] = full ring, contains everything + return true; + } else if (token.equals(minToken)) { + return !isStart; + } else { + return isStart + ? token.compareTo(range.getStart()) >= 0 + : token.compareTo(range.getStart()) > 0; + } + } else { + boolean isAfterStart = + isStart ? token.compareTo(range.getStart()) >= 0 : token.compareTo(range.getStart()) > 0; + boolean isBeforeEnd = + isStart ? token.compareTo(range.getEnd()) < 0 : token.compareTo(range.getEnd()) <= 0; + return range.isWrappedAround() + ? isAfterStart || isBeforeEnd // ####]----]#### + : isAfterStart && isBeforeEnd; // ----]####]---- + } + } + + @NonNull + @Override + public TokenRange mergeWith(@NonNull TokenRange that) { + if (this.equals(that)) { + return this; + } + + if (!(this.intersects(that) + || this.end.equals(that.getStart()) + || that.getEnd().equals(this.start))) { + throw new IllegalArgumentException( + String.format( + "Can't merge %s with %s because they neither intersect nor are adjacent", + this, that)); + } + + if (this.isEmpty()) { + return that; + } + + if (that.isEmpty()) { + return this; + } + + // That's actually "starts in or is adjacent to the end of" + boolean thisStartsInThat = contains(that, this.start, true) || this.start.equals(that.getEnd()); + boolean thatStartsInThis = + contains(this, that.getStart(), true) || that.getStart().equals(this.end); + + // This takes care of all the cases that return the full ring, so that we don't have to worry + // about them below + if (thisStartsInThat && thatStartsInThis) { + return fullRing(); + } + + // Starting at this.start, see how far we can go while staying in at least one of the ranges. + Token mergedEnd = + (thatStartsInThis && !contains(this, that.getEnd(), false)) ? that.getEnd() : this.end; + + // Repeat in the other direction. + Token mergedStart = thisStartsInThat ? that.getStart() : this.start; + + return newTokenRange(mergedStart, mergedEnd); + } + + private TokenRange fullRing() { + return newTokenRange(minToken, minToken); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof TokenRange) { + TokenRange that = (TokenRange) other; + return this.start.equals(that.getStart()) && this.end.equals(that.getEnd()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(start, end); + } + + @Override + public int compareTo(@NonNull TokenRange that) { + if (this.equals(that)) { + return 0; + } else { + int compareStart = this.start.compareTo(that.getStart()); + return compareStart != 0 ? compareStart : this.end.compareTo(that.getEnd()); + } + } + + @Override + public String toString() { + return String.format("%s(%s, %s)", getClass().getSimpleName(), start, end); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DefaultMetrics.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DefaultMetrics.java new file mode 100644 index 00000000000..6c9079bba8f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DefaultMetrics.java @@ -0,0 +1,63 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricRegistry; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.Metrics; +import com.datastax.oss.driver.api.core.metrics.NodeMetric; +import com.datastax.oss.driver.api.core.metrics.SessionMetric; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Optional; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DefaultMetrics implements Metrics { + + private final MetricRegistry registry; + private final DropwizardSessionMetricUpdater sessionUpdater; + + public DefaultMetrics(MetricRegistry registry, DropwizardSessionMetricUpdater sessionUpdater) { + this.registry = registry; + this.sessionUpdater = sessionUpdater; + } + + @NonNull + @Override + public MetricRegistry getRegistry() { + return registry; + } + + @NonNull + @Override + @SuppressWarnings("TypeParameterUnusedInFormals") + public Optional getSessionMetric( + @NonNull SessionMetric metric, String profileName) { + return Optional.ofNullable(sessionUpdater.getMetric(metric, profileName)); + } + + @NonNull + @Override + @SuppressWarnings("TypeParameterUnusedInFormals") + public Optional getNodeMetric( + @NonNull Node node, @NonNull NodeMetric metric, String profileName) { + NodeMetricUpdater nodeUpdater = ((DefaultNode) node).getMetricUpdater(); + return Optional.ofNullable( + ((DropwizardNodeMetricUpdater) nodeUpdater).getMetric(metric, profileName)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardMetricUpdater.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardMetricUpdater.java new file mode 100644 index 00000000000..0c47637d780 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardMetricUpdater.java @@ -0,0 +1,124 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.config.DriverOption; +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public abstract class DropwizardMetricUpdater implements MetricUpdater { + + private static final Logger LOG = LoggerFactory.getLogger(DropwizardMetricUpdater.class); + + protected final Set enabledMetrics; + protected final MetricRegistry registry; + + protected DropwizardMetricUpdater(Set enabledMetrics, MetricRegistry registry) { + this.enabledMetrics = enabledMetrics; + this.registry = registry; + } + + protected abstract String buildFullName(MetricT metric, String profileName); + + @Override + public void incrementCounter(MetricT metric, String profileName, long amount) { + if (isEnabled(metric, profileName)) { + registry.counter(buildFullName(metric, profileName)).inc(amount); + } + } + + @Override + public void updateHistogram(MetricT metric, String profileName, long value) { + if (isEnabled(metric, profileName)) { + registry.histogram(buildFullName(metric, profileName)).update(value); + } + } + + @Override + public void markMeter(MetricT metric, String profileName, long amount) { + if (isEnabled(metric, profileName)) { + registry.meter(buildFullName(metric, profileName)).mark(amount); + } + } + + @Override + public void updateTimer(MetricT metric, String profileName, long duration, TimeUnit unit) { + if (isEnabled(metric, profileName)) { + registry.timer(buildFullName(metric, profileName)).update(duration, unit); + } + } + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + public T getMetric(MetricT metric, String profileName) { + return (T) registry.getMetrics().get(buildFullName(metric, profileName)); + } + + @Override + public boolean isEnabled(MetricT metric, String profileName) { + return enabledMetrics.contains(metric); + } + + protected void initializeDefaultCounter(MetricT metric, String profileName) { + if (isEnabled(metric, profileName)) { + // Just initialize eagerly so that the metric appears even when it has no data yet + registry.counter(buildFullName(metric, profileName)); + } + } + + protected void initializeHdrTimer( + MetricT metric, + DriverExecutionProfile config, + DriverOption highestLatencyOption, + DriverOption significantDigitsOption, + DriverOption intervalOption) { + String profileName = config.getName(); + if (isEnabled(metric, profileName)) { + String fullName = buildFullName(metric, profileName); + + Duration highestLatency = config.getDuration(highestLatencyOption); + final int significantDigits; + int d = config.getInt(significantDigitsOption); + if (d >= 0 && d <= 5) { + significantDigits = d; + } else { + LOG.warn( + "[{}] Configuration option {} is out of range (expected between 0 and 5, found {}); " + + "using 3 instead.", + fullName, + significantDigitsOption, + d); + significantDigits = 3; + } + Duration refreshInterval = config.getDuration(intervalOption); + + // Initialize eagerly to use the custom implementation + registry.timer( + fullName, + () -> + new Timer( + new HdrReservoir(highestLatency, significantDigits, refreshInterval, fullName))); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardMetricsFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardMetricsFactory.java new file mode 100644 index 00000000000..76e9cb8965a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardMetricsFactory.java @@ -0,0 +1,114 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.codahale.metrics.MetricRegistry; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; +import com.datastax.oss.driver.api.core.metrics.Metrics; +import com.datastax.oss.driver.api.core.metrics.NodeMetric; +import com.datastax.oss.driver.api.core.metrics.SessionMetric; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class DropwizardMetricsFactory implements MetricsFactory { + + private static final Logger LOG = LoggerFactory.getLogger(DropwizardMetricsFactory.class); + + private final String logPrefix; + private final InternalDriverContext context; + private final Set enabledNodeMetrics; + private final MetricRegistry registry; + @Nullable private final Metrics metrics; + private final SessionMetricUpdater sessionUpdater; + + public DropwizardMetricsFactory(InternalDriverContext context) { + this.logPrefix = context.getSessionName(); + this.context = context; + + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + Set enabledSessionMetrics = + parseSessionMetricPaths(config.getStringList(DefaultDriverOption.METRICS_SESSION_ENABLED)); + this.enabledNodeMetrics = + parseNodeMetricPaths(config.getStringList(DefaultDriverOption.METRICS_NODE_ENABLED)); + + if (enabledSessionMetrics.isEmpty() && enabledNodeMetrics.isEmpty()) { + LOG.debug("[{}] All metrics are disabled, Session.getMetrics will be empty", logPrefix); + this.registry = null; + this.sessionUpdater = NoopSessionMetricUpdater.INSTANCE; + this.metrics = null; + } else { + this.registry = new MetricRegistry(); + DropwizardSessionMetricUpdater dropwizardSessionUpdater = + new DropwizardSessionMetricUpdater(enabledSessionMetrics, registry, context); + this.sessionUpdater = dropwizardSessionUpdater; + this.metrics = new DefaultMetrics(registry, dropwizardSessionUpdater); + } + } + + @Override + public Optional getMetrics() { + return Optional.ofNullable(metrics); + } + + @Override + public SessionMetricUpdater getSessionUpdater() { + return sessionUpdater; + } + + @Override + public NodeMetricUpdater newNodeUpdater(Node node) { + return (registry == null) + ? NoopNodeMetricUpdater.INSTANCE + : new DropwizardNodeMetricUpdater(node, enabledNodeMetrics, registry, context); + } + + protected Set parseSessionMetricPaths(List paths) { + EnumSet result = EnumSet.noneOf(DefaultSessionMetric.class); + for (String path : paths) { + try { + result.add(DefaultSessionMetric.fromPath(path)); + } catch (IllegalArgumentException e) { + LOG.warn("[{}] Unknown session metric {}, skipping", logPrefix, path); + } + } + return Collections.unmodifiableSet(result); + } + + protected Set parseNodeMetricPaths(List paths) { + EnumSet result = EnumSet.noneOf(DefaultNodeMetric.class); + for (String path : paths) { + try { + result.add(DefaultNodeMetric.fromPath(path)); + } catch (IllegalArgumentException e) { + LOG.warn("[{}] Unknown node metric {}, skipping", logPrefix, path); + } + } + return Collections.unmodifiableSet(result); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardNodeMetricUpdater.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardNodeMetricUpdater.java new file mode 100644 index 00000000000..a4322393e29 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardNodeMetricUpdater.java @@ -0,0 +1,111 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.MetricRegistry; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import com.datastax.oss.driver.api.core.metrics.NodeMetric; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.pool.ChannelPool; +import java.util.Set; +import java.util.function.Function; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DropwizardNodeMetricUpdater extends DropwizardMetricUpdater + implements NodeMetricUpdater { + + private final String metricNamePrefix; + + public DropwizardNodeMetricUpdater( + Node node, + Set enabledMetrics, + MetricRegistry registry, + InternalDriverContext context) { + super(enabledMetrics, registry); + this.metricNamePrefix = buildPrefix(context.getSessionName(), node.getEndPoint()); + + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + + if (enabledMetrics.contains(DefaultNodeMetric.OPEN_CONNECTIONS)) { + this.registry.register( + buildFullName(DefaultNodeMetric.OPEN_CONNECTIONS, null), + (Gauge) node::getOpenConnections); + } + initializePoolGauge( + DefaultNodeMetric.AVAILABLE_STREAMS, node, ChannelPool::getAvailableIds, context); + initializePoolGauge(DefaultNodeMetric.IN_FLIGHT, node, ChannelPool::getInFlight, context); + initializePoolGauge( + DefaultNodeMetric.ORPHANED_STREAMS, node, ChannelPool::getOrphanedIds, context); + initializeHdrTimer( + DefaultNodeMetric.CQL_MESSAGES, + config, + DefaultDriverOption.METRICS_NODE_CQL_MESSAGES_HIGHEST, + DefaultDriverOption.METRICS_NODE_CQL_MESSAGES_DIGITS, + DefaultDriverOption.METRICS_NODE_CQL_MESSAGES_INTERVAL); + initializeDefaultCounter(DefaultNodeMetric.UNSENT_REQUESTS, null); + initializeDefaultCounter(DefaultNodeMetric.ABORTED_REQUESTS, null); + initializeDefaultCounter(DefaultNodeMetric.WRITE_TIMEOUTS, null); + initializeDefaultCounter(DefaultNodeMetric.READ_TIMEOUTS, null); + initializeDefaultCounter(DefaultNodeMetric.UNAVAILABLES, null); + initializeDefaultCounter(DefaultNodeMetric.OTHER_ERRORS, null); + initializeDefaultCounter(DefaultNodeMetric.RETRIES, null); + initializeDefaultCounter(DefaultNodeMetric.RETRIES_ON_ABORTED, null); + initializeDefaultCounter(DefaultNodeMetric.RETRIES_ON_READ_TIMEOUT, null); + initializeDefaultCounter(DefaultNodeMetric.RETRIES_ON_WRITE_TIMEOUT, null); + initializeDefaultCounter(DefaultNodeMetric.RETRIES_ON_UNAVAILABLE, null); + initializeDefaultCounter(DefaultNodeMetric.RETRIES_ON_OTHER_ERROR, null); + initializeDefaultCounter(DefaultNodeMetric.IGNORES, null); + initializeDefaultCounter(DefaultNodeMetric.IGNORES_ON_ABORTED, null); + initializeDefaultCounter(DefaultNodeMetric.IGNORES_ON_READ_TIMEOUT, null); + initializeDefaultCounter(DefaultNodeMetric.IGNORES_ON_WRITE_TIMEOUT, null); + initializeDefaultCounter(DefaultNodeMetric.IGNORES_ON_UNAVAILABLE, null); + initializeDefaultCounter(DefaultNodeMetric.IGNORES_ON_OTHER_ERROR, null); + initializeDefaultCounter(DefaultNodeMetric.SPECULATIVE_EXECUTIONS, null); + initializeDefaultCounter(DefaultNodeMetric.CONNECTION_INIT_ERRORS, null); + initializeDefaultCounter(DefaultNodeMetric.AUTHENTICATION_ERRORS, null); + } + + @Override + public String buildFullName(NodeMetric metric, String profileName) { + return metricNamePrefix + metric.getPath(); + } + + private String buildPrefix(String sessionName, EndPoint endPoint) { + return sessionName + ".nodes." + endPoint.asMetricPrefix() + "."; + } + + private void initializePoolGauge( + NodeMetric metric, + Node node, + Function reading, + InternalDriverContext context) { + if (enabledMetrics.contains(metric)) { + registry.register( + buildFullName(metric, null), + (Gauge) + () -> { + ChannelPool pool = context.getPoolManager().getPools().get(node); + return (pool == null) ? 0 : reading.apply(pool); + }); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardSessionMetricUpdater.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardSessionMetricUpdater.java new file mode 100644 index 00000000000..3a81bcad221 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/DropwizardSessionMetricUpdater.java @@ -0,0 +1,139 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.codahale.metrics.Gauge; +import com.codahale.metrics.MetricRegistry; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; +import com.datastax.oss.driver.api.core.metrics.SessionMetric; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.cql.CqlPrepareAsyncProcessor; +import com.datastax.oss.driver.internal.core.cql.CqlPrepareSyncProcessor; +import com.datastax.oss.driver.internal.core.session.RequestProcessor; +import com.datastax.oss.driver.internal.core.session.throttling.ConcurrencyLimitingRequestThrottler; +import com.datastax.oss.driver.internal.core.session.throttling.RateLimitingRequestThrottler; +import com.datastax.oss.driver.shaded.guava.common.cache.Cache; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Set; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class DropwizardSessionMetricUpdater extends DropwizardMetricUpdater + implements SessionMetricUpdater { + + private static final Logger LOG = LoggerFactory.getLogger(DropwizardSessionMetricUpdater.class); + + private final String metricNamePrefix; + + public DropwizardSessionMetricUpdater( + Set enabledMetrics, MetricRegistry registry, InternalDriverContext context) { + super(enabledMetrics, registry); + this.metricNamePrefix = context.getSessionName() + "."; + + if (enabledMetrics.contains(DefaultSessionMetric.CONNECTED_NODES)) { + this.registry.gauge( + buildFullName(DefaultSessionMetric.CONNECTED_NODES, null), + () -> + () -> { + int count = 0; + for (Node node : context.getMetadataManager().getMetadata().getNodes().values()) { + if (node.getOpenConnections() > 0) { + count += 1; + } + } + return count; + }); + } + if (enabledMetrics.contains(DefaultSessionMetric.THROTTLING_QUEUE_SIZE)) { + this.registry.gauge( + buildFullName(DefaultSessionMetric.THROTTLING_QUEUE_SIZE, null), + () -> buildQueueGauge(context.getRequestThrottler(), context.getSessionName())); + } + if (enabledMetrics.contains(DefaultSessionMetric.CQL_PREPARED_CACHE_SIZE)) { + this.registry.gauge( + buildFullName(DefaultSessionMetric.CQL_PREPARED_CACHE_SIZE, null), + () -> { + Cache cache = getPreparedStatementCache(context); + Gauge gauge; + if (cache == null) { + LOG.warn( + "[{}] Metric {} is enabled in the config, " + + "but it looks like no CQL prepare processor is registered. " + + "The gauge will always return 0", + context.getSessionName(), + DefaultSessionMetric.CQL_PREPARED_CACHE_SIZE.getPath()); + gauge = () -> 0L; + } else { + gauge = cache::size; + } + return gauge; + }); + } + initializeHdrTimer( + DefaultSessionMetric.CQL_REQUESTS, + context.getConfig().getDefaultProfile(), + DefaultDriverOption.METRICS_SESSION_CQL_REQUESTS_HIGHEST, + DefaultDriverOption.METRICS_SESSION_CQL_REQUESTS_DIGITS, + DefaultDriverOption.METRICS_SESSION_CQL_REQUESTS_INTERVAL); + initializeDefaultCounter(DefaultSessionMetric.CQL_CLIENT_TIMEOUTS, null); + initializeHdrTimer( + DefaultSessionMetric.THROTTLING_DELAY, + context.getConfig().getDefaultProfile(), + DefaultDriverOption.METRICS_SESSION_THROTTLING_HIGHEST, + DefaultDriverOption.METRICS_SESSION_THROTTLING_DIGITS, + DefaultDriverOption.METRICS_SESSION_THROTTLING_INTERVAL); + initializeDefaultCounter(DefaultSessionMetric.THROTTLING_ERRORS, null); + } + + @Override + public String buildFullName(SessionMetric metric, String profileName) { + return metricNamePrefix + metric.getPath(); + } + + private Gauge buildQueueGauge(RequestThrottler requestThrottler, String logPrefix) { + if (requestThrottler instanceof ConcurrencyLimitingRequestThrottler) { + return ((ConcurrencyLimitingRequestThrottler) requestThrottler)::getQueueSize; + } else if (requestThrottler instanceof RateLimitingRequestThrottler) { + return ((RateLimitingRequestThrottler) requestThrottler)::getQueueSize; + } else { + LOG.warn( + "[{}] Metric {} does not support {}, it will always return 0", + logPrefix, + DefaultSessionMetric.THROTTLING_QUEUE_SIZE.getPath(), + requestThrottler.getClass().getName()); + return () -> 0; + } + } + + @Nullable + private static Cache getPreparedStatementCache(InternalDriverContext context) { + // By default, both the sync processor and the async one are registered and they share the same + // cache. But with a custom processor registry, there could be only one of the two present. + for (RequestProcessor processor : context.getRequestProcessorRegistry().getProcessors()) { + if (processor instanceof CqlPrepareAsyncProcessor) { + return ((CqlPrepareAsyncProcessor) processor).getCache(); + } else if (processor instanceof CqlPrepareSyncProcessor) { + return ((CqlPrepareSyncProcessor) processor).getCache(); + } + } + return null; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/HdrReservoir.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/HdrReservoir.java new file mode 100644 index 00000000000..44a004f8a60 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/HdrReservoir.java @@ -0,0 +1,268 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.codahale.metrics.Reservoir; +import com.codahale.metrics.Snapshot; +import java.io.OutputStream; +import java.time.Duration; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import net.jcip.annotations.GuardedBy; +import net.jcip.annotations.ThreadSafe; +import org.HdrHistogram.Histogram; +import org.HdrHistogram.Recorder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A reservoir implementation backed by the HdrHistogram library. + * + *

It uses a {@link Recorder} to capture snapshots at a configurable interval: calls to {@link + * #update(long)} are recorded in a "live" histogram, while {@link #getSnapshot()} is based on a + * "cached", read-only histogram. Each time the cached histogram becomes older than the interval, + * the two histograms are switched (therefore statistics won't be available during the first + * interval after initialization, since we don't have a cached histogram yet). + * + *

Note that this class does not implement {@link #size()}. + * + * @see HdrHistogram + */ +@ThreadSafe +public class HdrReservoir implements Reservoir { + + private static final Logger LOG = LoggerFactory.getLogger(HdrReservoir.class); + + private final String logPrefix; + private final Recorder recorder; + private final long refreshIntervalNanos; + + // The lock only orchestrates `getSnapshot()` calls; `update()` is fed directly to the recorder, + // which is lock-free. `getSnapshot()` calls are comparatively rare, so locking is not a + // bottleneck. + private final ReadWriteLock cacheLock = new ReentrantReadWriteLock(); + + @GuardedBy("cacheLock") + private Histogram cachedHistogram; + + @GuardedBy("cacheLock") + private long cachedHistogramTimestampNanos; + + @GuardedBy("cacheLock") + private Snapshot cachedSnapshot; + + public HdrReservoir( + Duration highestTrackableLatency, + int numberOfSignificantValueDigits, + Duration refreshInterval, + String logPrefix) { + this.logPrefix = logPrefix; + // The Reservoir interface is supposed to be agnostic to the unit. However, the Metrics library + // heavily leans towards nanoseconds (for example, Timer feeds nanoseconds to update(); JmxTimer + // assumes that the snapshot results are in nanoseconds). + // In our case, microseconds are precise enough for request metrics, and we don't want to waste + // space unnecessarily. So we simply use microseconds for our internal storage, and do the + // conversion when needed. + this.recorder = + new Recorder(highestTrackableLatency.toNanos() / 1000, numberOfSignificantValueDigits); + this.refreshIntervalNanos = refreshInterval.toNanos(); + this.cachedHistogramTimestampNanos = System.nanoTime(); + this.cachedSnapshot = EMPTY_SNAPSHOT; + } + + @Override + public void update(long value) { + try { + recorder.recordValue(value / 1000); + } catch (ArrayIndexOutOfBoundsException e) { + LOG.warn("[{}] Recorded value ({}) is out of bounds, discarding", logPrefix, value); + } + } + + /** + * Not implemented: this reservoir implementation is intended for use with a {@link + * com.codahale.metrics.Histogram}, which doesn't use this method. + * + *

(original description: {@inheritDoc}) + */ + @Override + public int size() { + throw new UnsupportedOperationException("HdrReservoir does not implement size()"); + } + + /** + * {@inheritDoc} + * + *

Note that the snapshots returned from this method do not implement {@link + * Snapshot#getValues()} nor {@link Snapshot#dump(OutputStream)}. In addition, due to the way that + * internal data structures are recycled, you should not hold onto a snapshot for more than the + * refresh interval; one way to ensure this is to never cache the result of this method. + */ + @Override + public Snapshot getSnapshot() { + long now = System.nanoTime(); + + cacheLock.readLock().lock(); + try { + if (now - cachedHistogramTimestampNanos < refreshIntervalNanos) { + return cachedSnapshot; + } + } finally { + cacheLock.readLock().unlock(); + } + + cacheLock.writeLock().lock(); + try { + // Might have raced with another writer => re-check the timestamp + if (now - cachedHistogramTimestampNanos >= refreshIntervalNanos) { + LOG.debug("Cached snapshot is too old, refreshing"); + cachedHistogram = recorder.getIntervalHistogram(cachedHistogram); + cachedSnapshot = new HdrSnapshot(cachedHistogram); + cachedHistogramTimestampNanos = now; + } + return cachedSnapshot; + } finally { + cacheLock.writeLock().unlock(); + } + } + + private class HdrSnapshot extends Snapshot { + + private final Histogram histogram; + private final double meanNanos; + private final double stdDevNanos; + + private HdrSnapshot(Histogram histogram) { + this.histogram = histogram; + + // Cache those values because they rely on HdrHistogram's internal iterators, which are not + // safe if the snapshot is accessed by concurrent reporters. + // In contrast, getMin(), getMax() and getValue() are safe. + this.meanNanos = histogram.getMean() * 1000; + this.stdDevNanos = histogram.getStdDeviation() * 1000; + } + + @Override + public double getValue(double quantile) { + return histogram.getValueAtPercentile(quantile * 100) * 1000; + } + + /** + * Not implemented: this reservoir implementation is intended for use with a {@link + * com.codahale.metrics.Histogram}, which doesn't use this method. + * + *

(original description: {@inheritDoc}) + */ + @Override + public long[] getValues() { + // This can be implemented, but we ran into issues when accessed by concurrent reporters + // because HdrHistogram uses an unsafe shared iterator. + // So throwing instead since this method should be seldom used anyway. + throw new UnsupportedOperationException( + "HdrReservoir's snapshots do not implement getValues()"); + } + + @Override + public int size() { + long longSize = histogram.getTotalCount(); + // The Metrics API requires an int. It's very unlikely that we get an overflow here, unless + // the refresh interval is ridiculously high (at 10k requests/s, it would have to be more than + // 59 hours). However handle gracefully just in case. + int size; + if (longSize > Integer.MAX_VALUE) { + LOG.warn("[{}] Too many recorded values, truncating", logPrefix); + size = Integer.MAX_VALUE; + } else { + size = (int) longSize; + } + return size; + } + + @Override + public long getMax() { + return histogram.getMaxValue() * 1000; + } + + @Override + public double getMean() { + return meanNanos; + } + + @Override + public long getMin() { + return histogram.getMinValue() * 1000; + } + + @Override + public double getStdDev() { + return stdDevNanos; + } + + /** + * Not implemented: this reservoir implementation is intended for use with a {@link + * com.codahale.metrics.Histogram}, which doesn't use this method. + * + *

(original description: {@inheritDoc}) + */ + @Override + public void dump(OutputStream output) { + throw new UnsupportedOperationException("HdrReservoir's snapshots do not implement dump()"); + } + } + + private static final Snapshot EMPTY_SNAPSHOT = + new Snapshot() { + @Override + public double getValue(double quantile) { + return 0; + } + + @Override + public long[] getValues() { + return new long[0]; + } + + @Override + public int size() { + return 0; + } + + @Override + public long getMax() { + return 0; + } + + @Override + public double getMean() { + return 0; + } + + @Override + public long getMin() { + return 0; + } + + @Override + public double getStdDev() { + return 0; + } + + @Override + public void dump(OutputStream output) { + // nothing to do + } + }; +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/MetricUpdater.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/MetricUpdater.java new file mode 100644 index 00000000000..f8dc93460b5 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/MetricUpdater.java @@ -0,0 +1,45 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import java.util.concurrent.TimeUnit; + +/** + * Note about profiles names: they are included to keep the possibility to break up metrics per + * profile in the future, but right now the default updater implementations ignore them. The driver + * internals provide a profile name when it makes sense and is practical; in other cases, it passes + * {@code null}. + */ +public interface MetricUpdater { + + void incrementCounter(MetricT metric, String profileName, long amount); + + default void incrementCounter(MetricT metric, String profileName) { + incrementCounter(metric, profileName, 1); + } + + void updateHistogram(MetricT metric, String profileName, long value); + + void markMeter(MetricT metric, String profileName, long amount); + + default void markMeter(MetricT metric, String profileName) { + markMeter(metric, profileName, 1); + } + + void updateTimer(MetricT metric, String profileName, long duration, TimeUnit unit); + + boolean isEnabled(MetricT metric, String profileName); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/MetricsFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/MetricsFactory.java new file mode 100644 index 00000000000..26440c42b6c --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/MetricsFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.Metrics; +import java.util.Optional; + +public interface MetricsFactory { + + Optional getMetrics(); + + /** @return the unique instance for this session (this must return the same object every time). */ + SessionMetricUpdater getSessionUpdater(); + + NodeMetricUpdater newNodeUpdater(Node node); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/NodeMetricUpdater.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/NodeMetricUpdater.java new file mode 100644 index 00000000000..4a145124d6a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/NodeMetricUpdater.java @@ -0,0 +1,20 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.datastax.oss.driver.api.core.metrics.NodeMetric; + +public interface NodeMetricUpdater extends MetricUpdater {} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/NoopNodeMetricUpdater.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/NoopNodeMetricUpdater.java new file mode 100644 index 00000000000..df0f8c9bfff --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/NoopNodeMetricUpdater.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.datastax.oss.driver.api.core.metrics.NodeMetric; +import java.util.concurrent.TimeUnit; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class NoopNodeMetricUpdater implements NodeMetricUpdater { + + public static NoopNodeMetricUpdater INSTANCE = new NoopNodeMetricUpdater(); + + private NoopNodeMetricUpdater() {} + + @Override + public void incrementCounter(NodeMetric metric, String profileName, long amount) { + // nothing to do + } + + @Override + public void updateHistogram(NodeMetric metric, String profileName, long value) { + // nothing to do + } + + @Override + public void markMeter(NodeMetric metric, String profileName, long amount) { + // nothing to do + } + + @Override + public void updateTimer(NodeMetric metric, String profileName, long duration, TimeUnit unit) { + // nothing to do + } + + @Override + public boolean isEnabled(NodeMetric metric, String profileName) { + // since methods don't do anything, return false + return false; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/NoopSessionMetricUpdater.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/NoopSessionMetricUpdater.java new file mode 100644 index 00000000000..a4cd33b12b7 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/NoopSessionMetricUpdater.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.datastax.oss.driver.api.core.metrics.SessionMetric; +import java.util.concurrent.TimeUnit; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class NoopSessionMetricUpdater implements SessionMetricUpdater { + + public static NoopSessionMetricUpdater INSTANCE = new NoopSessionMetricUpdater(); + + private NoopSessionMetricUpdater() {} + + @Override + public void incrementCounter(SessionMetric metric, String profileName, long amount) { + // nothing to do + } + + @Override + public void updateHistogram(SessionMetric metric, String profileName, long value) { + // nothing to do + } + + @Override + public void markMeter(SessionMetric metric, String profileName, long amount) { + // nothing to do + } + + @Override + public void updateTimer(SessionMetric metric, String profileName, long duration, TimeUnit unit) { + // nothing to do + } + + @Override + public boolean isEnabled(SessionMetric metric, String profileName) { + // since methods don't do anything, return false + return false; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/SessionMetricUpdater.java b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/SessionMetricUpdater.java new file mode 100644 index 00000000000..8aaf82dcb71 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/metrics/SessionMetricUpdater.java @@ -0,0 +1,20 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metrics; + +import com.datastax.oss.driver.api.core.metrics.SessionMetric; + +public interface SessionMetricUpdater extends MetricUpdater {} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/os/Native.java b/core/src/main/java/com/datastax/oss/driver/internal/core/os/Native.java new file mode 100644 index 00000000000..25df2d5d23a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/os/Native.java @@ -0,0 +1,201 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.os; + +import java.lang.reflect.Method; +import jnr.ffi.LibraryLoader; +import jnr.ffi.Platform; +import jnr.ffi.Pointer; +import jnr.ffi.Runtime; +import jnr.ffi.Struct; +import jnr.ffi.annotations.Out; +import jnr.ffi.annotations.Transient; +import jnr.posix.POSIXFactory; +import jnr.posix.util.DefaultPOSIXHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** A gateway to perform system calls. */ +public class Native { + + private static final Logger LOG = LoggerFactory.getLogger(Native.class); + + /** Whether {@link Native#currentTimeMicros()} is available on this system. */ + public static boolean isCurrentTimeMicrosAvailable() { + try { + return LibCLoader.GET_TIME_OF_DAY_AVAILABLE; + } catch (NoClassDefFoundError e) { + return false; + } + } + + /** + * The current time in microseconds, as returned by libc.gettimeofday(); can only be used if + * {@link #isCurrentTimeMicrosAvailable()} is true. + */ + public static long currentTimeMicros() { + if (!isCurrentTimeMicrosAvailable()) { + throw new IllegalStateException( + "Native call not available. " + + "Check isCurrentTimeMicrosAvailable() before calling this method."); + } + LibCLoader.Timeval tv = new LibCLoader.Timeval(LibCLoader.LIB_C_RUNTIME); + int res = LibCLoader.LIB_C.gettimeofday(tv, null); + if (res != 0) { + throw new IllegalStateException("Call to libc.gettimeofday() failed with result " + res); + } + return tv.tv_sec.get() * 1000000 + tv.tv_usec.get(); + } + + public static boolean isGetProcessIdAvailable() { + try { + return PosixLoader.GET_PID_AVAILABLE; + } catch (NoClassDefFoundError e) { + return false; + } + } + + public static int getProcessId() { + if (!isGetProcessIdAvailable()) { + throw new IllegalStateException( + "Native call not available. " + + "Check isGetProcessIdAvailable() before calling this method."); + } + return PosixLoader.POSIX.getpid(); + } + + /** + * Returns {@code true} if JNR {@link Platform} class is loaded, and {@code false} otherwise. + * + * @return {@code true} if JNR {@link Platform} class is loaded. + */ + public static boolean isPlatformAvailable() { + try { + return PlatformLoader.PLATFORM != null; + } catch (NoClassDefFoundError e) { + return false; + } + } + + /** + * Returns the current processor architecture the JVM is running on, as reported by {@link + * Platform#getCPU()}. + * + * @return the current processor architecture. + * @throws IllegalStateException if JNR Platform library is not loaded. + */ + public static String getCPU() { + if (!isPlatformAvailable()) + throw new IllegalStateException( + "JNR Platform class not loaded. " + + "Check isPlatformAvailable() before calling this method."); + return PlatformLoader.PLATFORM.getCPU().toString(); + } + + /** + * If jnr-ffi is not in the classpath at runtime, we'll fail to initialize the static fields + * below, but we still want {@link Native} to initialize successfully, so use an inner class. + */ + private static class LibCLoader { + + /** Handles libc calls through JNR (must be public). */ + public interface LibC { + int gettimeofday(@Out @Transient Timeval tv, Pointer unused); + } + + // See http://man7.org/linux/man-pages/man2/settimeofday.2.html + private static class Timeval extends Struct { + private final time_t tv_sec = new time_t(); + private final Unsigned32 tv_usec = new Unsigned32(); + + private Timeval(Runtime runtime) { + super(runtime); + } + } + + private static final LibC LIB_C; + private static final Runtime LIB_C_RUNTIME; + private static final boolean GET_TIME_OF_DAY_AVAILABLE; + + static { + LibC libc; + Runtime runtime = null; + try { + libc = LibraryLoader.create(LibC.class).load("c"); + runtime = Runtime.getRuntime(libc); + } catch (Throwable t) { + libc = null; + LOG.debug("Error loading libc", t); + } + LIB_C = libc; + LIB_C_RUNTIME = runtime; + boolean getTimeOfDayAvailable = false; + if (LIB_C_RUNTIME != null) { + try { + getTimeOfDayAvailable = LIB_C.gettimeofday(new Timeval(LIB_C_RUNTIME), null) == 0; + } catch (Throwable t) { + LOG.debug("Error accessing libc.gettimeofday()", t); + } + } + GET_TIME_OF_DAY_AVAILABLE = getTimeOfDayAvailable; + } + } + + /** @see LibCLoader */ + private static class PosixLoader { + private static final jnr.posix.POSIX POSIX; + private static final boolean GET_PID_AVAILABLE; + + static { + jnr.posix.POSIX posix; + try { + posix = POSIXFactory.getPOSIX(new DefaultPOSIXHandler(), true); + } catch (Throwable t) { + posix = null; + LOG.debug("Error loading POSIX", t); + } + POSIX = posix; + boolean getPidAvailable = false; + if (POSIX != null) { + try { + POSIX.getpid(); + getPidAvailable = true; + } catch (Throwable t) { + LOG.debug("Error accessing posix.getpid()", t); + } + } + GET_PID_AVAILABLE = getPidAvailable; + } + } + + private static class PlatformLoader { + + private static final Platform PLATFORM; + + static { + Platform platform; + try { + Class platformClass = Class.forName("jnr.ffi.Platform"); + Method getNativePlatform = platformClass.getMethod("getNativePlatform"); + platform = (Platform) getNativePlatform.invoke(null); + } catch (Throwable t) { + platform = null; + LOG.debug("Error loading jnr.ffi.Platform class, this class will not be available.", t); + } + PLATFORM = platform; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/pool/ChannelPool.java b/core/src/main/java/com/datastax/oss/driver/internal/core/pool/ChannelPool.java new file mode 100644 index 00000000000..24891972763 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/pool/ChannelPool.java @@ -0,0 +1,584 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.pool; + +import com.datastax.oss.driver.api.core.AsyncAutoCloseable; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.InvalidKeyspaceException; +import com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException; +import com.datastax.oss.driver.api.core.auth.AuthenticationException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import com.datastax.oss.driver.internal.core.channel.ChannelEvent; +import com.datastax.oss.driver.internal.core.channel.ChannelFactory; +import com.datastax.oss.driver.internal.core.channel.ClusterNameMismatchException; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.DriverChannelOptions; +import com.datastax.oss.driver.internal.core.config.ConfigChangeEvent; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.metadata.TopologyEvent; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.internal.core.util.concurrent.Reconnection; +import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule; +import com.datastax.oss.driver.internal.core.util.concurrent.UncaughtExceptions; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.Sets; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GenericFutureListener; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The channel pool maintains a set of {@link DriverChannel} instances connected to a given node. + * + *

It allows clients to obtain a channel to execute their requests. + * + *

If one or more channels go down, a reconnection process starts in order to replace them; it + * runs until the channel count is back to its intended target. + */ +@ThreadSafe +public class ChannelPool implements AsyncAutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(ChannelPool.class); + + /** + * Initializes a new pool. + * + *

The returned completion stage will complete when all the underlying channels have finished + * their initialization. If one or more channels fail, a reconnection will be started immediately. + * Note that this method succeeds even if all channels fail, so you might get a pool that has no + * channels (i.e. {@link #next()} return {@code null}) and is reconnecting. + */ + public static CompletionStage init( + Node node, + CqlIdentifier keyspaceName, + NodeDistance distance, + InternalDriverContext context, + String sessionLogPrefix) { + ChannelPool pool = new ChannelPool(node, keyspaceName, distance, context, sessionLogPrefix); + return pool.connect(); + } + + // This is read concurrently, but only mutated on adminExecutor (by methods in SingleThreaded) + @VisibleForTesting final ChannelSet channels = new ChannelSet(); + + private final Node node; + private final CqlIdentifier initialKeyspaceName; + private final EventExecutor adminExecutor; + private final String sessionLogPrefix; + private final String logPrefix; + private final SingleThreaded singleThreaded; + private volatile boolean invalidKeyspace; + + private ChannelPool( + Node node, + CqlIdentifier keyspaceName, + NodeDistance distance, + InternalDriverContext context, + String sessionLogPrefix) { + this.node = node; + this.initialKeyspaceName = keyspaceName; + this.adminExecutor = context.getNettyOptions().adminEventExecutorGroup().next(); + this.sessionLogPrefix = sessionLogPrefix; + this.logPrefix = sessionLogPrefix + "|" + node.getEndPoint(); + this.singleThreaded = new SingleThreaded(keyspaceName, distance, context); + } + + private CompletionStage connect() { + RunOrSchedule.on(adminExecutor, singleThreaded::connect); + return singleThreaded.connectFuture; + } + + public Node getNode() { + return node; + } + + /** + * The keyspace with which the pool was initialized (therefore a constant, it's not updated if the + * keyspace is switched later). + */ + public CqlIdentifier getInitialKeyspaceName() { + return initialKeyspaceName; + } + + /** + * Whether all channels failed due to an invalid keyspace. This is only used at initialization. We + * don't make the decision to close the pool here yet, that's done at the session level. + */ + public boolean isInvalidKeyspace() { + return invalidKeyspace; + } + + /** + * @return the channel that has the most available stream ids. This is called on the direct + * request path, and we want to avoid complex check-then-act semantics; therefore this might + * race and return a channel that is already closed, or {@code null}. In those cases, it is up + * to the caller to fail fast and move to the next node. + *

There is no need to return the channel. + */ + public DriverChannel next() { + return channels.next(); + } + + /** @return the number of active channels in the pool. */ + public int size() { + return channels.size(); + } + + /** @return the number of available stream ids on all channels in the pool. */ + public int getAvailableIds() { + return channels.getAvailableIds(); + } + + /** + * @return the number of requests currently executing on all channels in this pool (including + * {@link #getOrphanedIds() orphaned ids}). + */ + public int getInFlight() { + return channels.getInFlight(); + } + + /** + * @return the number of stream ids for requests in all channels in this pool that have either + * timed out or been cancelled, but for which we can't release the stream id because a request + * might still come from the server. + */ + public int getOrphanedIds() { + return channels.getOrphanedIds(); + } + + /** + * Sets a new distance for the node this pool belongs to. This method returns immediately, the new + * distance will be set asynchronously. + * + * @param newDistance the new distance to set. + */ + public void resize(NodeDistance newDistance) { + RunOrSchedule.on(adminExecutor, () -> singleThreaded.resize(newDistance)); + } + + /** + * Changes the keyspace name on all the channels in this pool. + * + *

Note that this is not called directly by the user, but happens only on a SetKeyspace + * response after a successful "USE ..." query, so the name should be valid. If the keyspace + * switch fails on any channel, that channel is closed and a reconnection is started. + */ + public CompletionStage setKeyspace(CqlIdentifier newKeyspaceName) { + return RunOrSchedule.on(adminExecutor, () -> singleThreaded.setKeyspace(newKeyspaceName)); + } + + public void reconnectNow() { + RunOrSchedule.on(adminExecutor, singleThreaded::reconnectNow); + } + + @NonNull + @Override + public CompletionStage closeFuture() { + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage closeAsync() { + RunOrSchedule.on(adminExecutor, singleThreaded::close); + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage forceCloseAsync() { + RunOrSchedule.on(adminExecutor, singleThreaded::forceClose); + return singleThreaded.closeFuture; + } + + /** Holds all administration tasks, that are confined to the admin executor. */ + private class SingleThreaded { + + private final DriverConfig config; + private final ChannelFactory channelFactory; + private final EventBus eventBus; + // The channels that are currently connecting + private final List> pendingChannels = new ArrayList<>(); + private final Set closingChannels = new HashSet<>(); + private final Reconnection reconnection; + private final Object configListenerKey; + + private NodeDistance distance; + private int wantedCount; + private final CompletableFuture connectFuture = new CompletableFuture<>(); + private boolean isConnecting; + private final CompletableFuture closeFuture = new CompletableFuture<>(); + private boolean isClosing; + private CompletableFuture setKeyspaceFuture; + + private CqlIdentifier keyspaceName; + + private SingleThreaded( + CqlIdentifier keyspaceName, NodeDistance distance, InternalDriverContext context) { + this.keyspaceName = keyspaceName; + this.config = context.getConfig(); + this.distance = distance; + this.wantedCount = getConfiguredSize(distance); + this.channelFactory = context.getChannelFactory(); + this.eventBus = context.getEventBus(); + ReconnectionPolicy reconnectionPolicy = context.getReconnectionPolicy(); + this.reconnection = + new Reconnection( + logPrefix, + adminExecutor, + () -> reconnectionPolicy.newNodeSchedule(node), + this::addMissingChannels, + () -> eventBus.fire(ChannelEvent.reconnectionStarted(node)), + () -> eventBus.fire(ChannelEvent.reconnectionStopped(node))); + this.configListenerKey = + eventBus.register( + ConfigChangeEvent.class, RunOrSchedule.on(adminExecutor, this::onConfigChanged)); + } + + private void connect() { + assert adminExecutor.inEventLoop(); + if (isConnecting) { + return; + } + isConnecting = true; + CompletionStage initialChannels = + addMissingChannels() + .thenApply( + allConnected -> { + if (!allConnected) { + reconnection.start(); + } + return ChannelPool.this; + }); + CompletableFutures.completeFrom(initialChannels, connectFuture); + } + + private CompletionStage addMissingChannels() { + assert adminExecutor.inEventLoop(); + // We always wait for all attempts to succeed or fail before scheduling a reconnection + assert pendingChannels.isEmpty(); + + int missing = wantedCount - channels.size(); + LOG.debug("[{}] Trying to create {} missing channels", logPrefix, missing); + DriverChannelOptions options = + DriverChannelOptions.builder() + .withKeyspace(keyspaceName) + .withOwnerLogPrefix(sessionLogPrefix) + .build(); + for (int i = 0; i < missing; i++) { + CompletionStage channelFuture = channelFactory.connect(node, options); + pendingChannels.add(channelFuture); + } + return CompletableFutures.allDone(pendingChannels) + .thenApplyAsync(this::onAllConnected, adminExecutor); + } + + private boolean onAllConnected(@SuppressWarnings("unused") Void v) { + assert adminExecutor.inEventLoop(); + Throwable fatalError = null; + int invalidKeyspaceErrors = 0; + for (CompletionStage pendingChannel : pendingChannels) { + CompletableFuture future = pendingChannel.toCompletableFuture(); + assert future.isDone(); + if (future.isCompletedExceptionally()) { + Throwable error = CompletableFutures.getFailed(future); + ((DefaultNode) node) + .getMetricUpdater() + .incrementCounter( + error instanceof AuthenticationException + ? DefaultNodeMetric.AUTHENTICATION_ERRORS + : DefaultNodeMetric.CONNECTION_INIT_ERRORS, + null); + if (error instanceof ClusterNameMismatchException + || error instanceof UnsupportedProtocolVersionException) { + // This will likely be thrown by all channels, but finish the loop cleanly + fatalError = error; + } else if (error instanceof AuthenticationException) { + // Always warn because this is most likely something the operator needs to fix. + // Keep going to reconnect if it can be fixed without bouncing the client. + Loggers.warnWithException(LOG, "[{}] Authentication error", logPrefix, error); + } else if (error instanceof InvalidKeyspaceException) { + invalidKeyspaceErrors += 1; + } else { + if (config + .getDefaultProfile() + .getBoolean(DefaultDriverOption.CONNECTION_WARN_INIT_ERROR)) { + Loggers.warnWithException( + LOG, "[{}] Error while opening new channel", logPrefix, error); + } else { + LOG.debug("[{}] Error while opening new channel", logPrefix, error); + } + } + } else { + DriverChannel channel = CompletableFutures.getCompleted(future); + if (isClosing) { + LOG.debug( + "[{}] New channel added ({}) but the pool was closed, closing it", + logPrefix, + channel); + channel.forceClose(); + } else { + LOG.debug("[{}] New channel added {}", logPrefix, channel); + channels.add(channel); + eventBus.fire(ChannelEvent.channelOpened(node)); + channel + .closeStartedFuture() + .addListener( + f -> + adminExecutor + .submit(() -> onChannelCloseStarted(channel)) + .addListener(UncaughtExceptions::log)); + channel + .closeFuture() + .addListener( + f -> + adminExecutor + .submit(() -> onChannelClosed(channel)) + .addListener(UncaughtExceptions::log)); + } + } + } + // If all channels failed, assume the keyspace is wrong + invalidKeyspace = + invalidKeyspaceErrors > 0 && invalidKeyspaceErrors == pendingChannels.size(); + + pendingChannels.clear(); + + if (fatalError != null) { + Loggers.warnWithException( + LOG, + "[{}] Fatal error while initializing pool, forcing the node down", + logPrefix, + fatalError); + // Note: getBroadcastRpcAddress() can only be empty for the control node (and not for modern + // C* versions anyway). If we already have a control connection open to that node, it's + // impossible to get a protocol version or cluster name mismatch error while creating the + // pool, so it's safe to ignore this case. + node.getBroadcastRpcAddress() + .ifPresent(address -> eventBus.fire(TopologyEvent.forceDown(address))); + // Don't bother continuing, the pool will get shut down soon anyway + return true; + } + + shrinkIfTooManyChannels(); // Can happen if the pool was shrinked during the reconnection + + int currentCount = channels.size(); + LOG.debug( + "[{}] Reconnection attempt complete, {}/{} channels", + logPrefix, + currentCount, + wantedCount); + // Stop reconnecting if we have the wanted count + return currentCount >= wantedCount; + } + + private void onChannelCloseStarted(DriverChannel channel) { + assert adminExecutor.inEventLoop(); + if (!isClosing) { + LOG.debug("[{}] Channel {} started graceful shutdown", logPrefix, channel); + channels.remove(channel); + closingChannels.add(channel); + eventBus.fire(ChannelEvent.channelClosed(node)); + reconnection.start(); + } + } + + private void onChannelClosed(DriverChannel channel) { + assert adminExecutor.inEventLoop(); + if (!isClosing) { + // Either it was closed abruptly and was still in the live set, or it was an orderly + // shutdown and it had moved to the closing set. + if (channels.remove(channel)) { + LOG.debug("[{}] Lost channel {}", logPrefix, channel); + eventBus.fire(ChannelEvent.channelClosed(node)); + reconnection.start(); + } else { + LOG.debug("[{}] Channel {} completed graceful shutdown", logPrefix, channel); + closingChannels.remove(channel); + } + } + } + + private void resize(NodeDistance newDistance) { + assert adminExecutor.inEventLoop(); + distance = newDistance; + int newChannelCount = getConfiguredSize(newDistance); + if (newChannelCount > wantedCount) { + LOG.debug("[{}] Growing ({} => {} channels)", logPrefix, wantedCount, newChannelCount); + wantedCount = newChannelCount; + reconnection.start(); + } else if (newChannelCount < wantedCount) { + LOG.debug("[{}] Shrinking ({} => {} channels)", logPrefix, wantedCount, newChannelCount); + wantedCount = newChannelCount; + if (!reconnection.isRunning()) { + shrinkIfTooManyChannels(); + } // else it will be handled at the end of the reconnection attempt + } + } + + private void shrinkIfTooManyChannels() { + assert adminExecutor.inEventLoop(); + int extraCount = channels.size() - wantedCount; + if (extraCount > 0) { + LOG.debug("[{}] Closing {} extra channels", logPrefix, extraCount); + Set toRemove = Sets.newHashSetWithExpectedSize(extraCount); + for (DriverChannel channel : channels) { + toRemove.add(channel); + if (--extraCount == 0) { + break; + } + } + for (DriverChannel channel : toRemove) { + channels.remove(channel); + channel.close(); + eventBus.fire(ChannelEvent.channelClosed(node)); + } + } + } + + private void onConfigChanged(@SuppressWarnings("unused") ConfigChangeEvent event) { + assert adminExecutor.inEventLoop(); + // resize re-reads the pool size from the configuration and does nothing if it hasn't changed, + // which is exactly what we want. + resize(distance); + } + + private CompletionStage setKeyspace(CqlIdentifier newKeyspaceName) { + assert adminExecutor.inEventLoop(); + if (setKeyspaceFuture != null && !setKeyspaceFuture.isDone()) { + return CompletableFutures.failedFuture( + new IllegalStateException( + "Can't call setKeyspace while a keyspace switch is already in progress")); + } + keyspaceName = newKeyspaceName; + setKeyspaceFuture = new CompletableFuture<>(); + + // Switch the keyspace on all live channels. + // We can read the size before iterating because mutations are confined to this thread: + int toSwitch = channels.size(); + if (toSwitch == 0) { + setKeyspaceFuture.complete(null); + } else { + AtomicInteger remaining = new AtomicInteger(toSwitch); + for (DriverChannel channel : channels) { + channel + .setKeyspace(newKeyspaceName) + .addListener( + f -> { + // Don't handle errors: if a channel fails to switch the keyspace, it closes + if (remaining.decrementAndGet() == 0) { + setKeyspaceFuture.complete(null); + } + }); + } + } + + // pending channels were scheduled with the old keyspace name, ensure they eventually switch + for (CompletionStage channelFuture : pendingChannels) { + // errors are swallowed here, this is fine because a setkeyspace error will close the + // channel, so it will eventually get reported + channelFuture.thenAccept(channel -> channel.setKeyspace(newKeyspaceName)); + } + + return setKeyspaceFuture; + } + + private void reconnectNow() { + assert adminExecutor.inEventLoop(); + // Don't force because if the reconnection is stopped, it means either we have enough channels + // or the pool is shutting down. + reconnection.reconnectNow(false); + } + + private void close() { + assert adminExecutor.inEventLoop(); + if (isClosing) { + return; + } + isClosing = true; + + // If an attempt was in progress right now, it might open new channels but they will be + // handled in onAllConnected + reconnection.stop(); + + eventBus.unregister(configListenerKey, ConfigChangeEvent.class); + + // Close all channels, the pool future completes when all the channels futures have completed + int toClose = closingChannels.size() + channels.size(); + if (toClose == 0) { + closeFuture.complete(null); + } else { + AtomicInteger remaining = new AtomicInteger(toClose); + GenericFutureListener> channelCloseListener = + f -> { + if (!f.isSuccess()) { + Loggers.warnWithException(LOG, "[{}] Error closing channel", logPrefix, f.cause()); + } + if (remaining.decrementAndGet() == 0) { + closeFuture.complete(null); + } + }; + for (DriverChannel channel : channels) { + eventBus.fire(ChannelEvent.channelClosed(node)); + channel.close().addListener(channelCloseListener); + } + for (DriverChannel channel : closingChannels) { + // don't fire the close event, onChannelCloseStarted() already did it + channel.closeFuture().addListener(channelCloseListener); + } + } + } + + private void forceClose() { + assert adminExecutor.inEventLoop(); + if (!isClosing) { + close(); + } + for (DriverChannel channel : channels) { + channel.forceClose(); + } + for (DriverChannel channel : closingChannels) { + channel.forceClose(); + } + } + + private int getConfiguredSize(NodeDistance distance) { + return config + .getDefaultProfile() + .getInt( + (distance == NodeDistance.LOCAL) + ? DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE + : DefaultDriverOption.CONNECTION_POOL_REMOTE_SIZE); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolFactory.java new file mode 100644 index 00000000000..24ba12ac3bc --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.pool; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import java.util.concurrent.CompletionStage; +import net.jcip.annotations.ThreadSafe; + +/** Just a level of indirection to make testing easier. */ +@ThreadSafe +public class ChannelPoolFactory { + public CompletionStage init( + Node node, + CqlIdentifier keyspaceName, + NodeDistance distance, + InternalDriverContext context, + String sessionLogPrefix) { + return ChannelPool.init(node, keyspaceName, distance, context, sessionLogPrefix); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/pool/ChannelSet.java b/core/src/main/java/com/datastax/oss/driver/internal/core/pool/ChannelSet.java new file mode 100644 index 00000000000..0f6144c77a4 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/pool/ChannelSet.java @@ -0,0 +1,156 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.pool; + +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterators; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Arrays; +import java.util.Iterator; +import java.util.concurrent.locks.ReentrantLock; +import net.jcip.annotations.ThreadSafe; + +/** + * Concurrent structure used to store the channels of a pool. + * + *

Its write semantics are similar to "copy-on-write" JDK collections, selection operations are + * expected to vastly outnumber mutations. + */ +@ThreadSafe +class ChannelSet implements Iterable { + private volatile DriverChannel[] channels; + private final ReentrantLock lock = new ReentrantLock(); // must be held when mutating the array + + ChannelSet() { + this.channels = new DriverChannel[] {}; + } + + void add(DriverChannel toAdd) { + Preconditions.checkNotNull(toAdd); + lock.lock(); + try { + assert indexOf(channels, toAdd) < 0; + DriverChannel[] newChannels = Arrays.copyOf(channels, channels.length + 1); + newChannels[newChannels.length - 1] = toAdd; + channels = newChannels; + } finally { + lock.unlock(); + } + } + + boolean remove(DriverChannel toRemove) { + Preconditions.checkNotNull(toRemove); + lock.lock(); + try { + int index = indexOf(channels, toRemove); + if (index < 0) { + return false; + } else { + DriverChannel[] newChannels = new DriverChannel[channels.length - 1]; + int newI = 0; + for (int i = 0; i < channels.length; i++) { + if (i != index) { + newChannels[newI] = channels[i]; + newI += 1; + } + } + channels = newChannels; + return true; + } + } finally { + lock.unlock(); + } + } + + /** @return null if the set is empty or all are full */ + DriverChannel next() { + DriverChannel[] snapshot = this.channels; + switch (snapshot.length) { + case 0: + return null; + case 1: + return snapshot[0]; + default: + DriverChannel best = null; + int bestScore = 0; + for (DriverChannel channel : snapshot) { + int score = channel.getAvailableIds(); + if (score > bestScore) { + bestScore = score; + best = channel; + } + } + return best; + } + } + + /** @return the number of available stream ids on all channels in this channel set. */ + int getAvailableIds() { + int availableIds = 0; + DriverChannel[] snapshot = this.channels; + for (DriverChannel channel : snapshot) { + availableIds += channel.getAvailableIds(); + } + return availableIds; + } + + /** + * @return the number of requests currently executing on all channels in this channel set + * (including {@link #getOrphanedIds() orphaned ids}). + */ + int getInFlight() { + int inFlight = 0; + DriverChannel[] snapshot = this.channels; + for (DriverChannel channel : snapshot) { + inFlight += channel.getInFlight(); + } + return inFlight; + } + + /** + * @return the number of stream ids for requests in all channels in this channel set that have + * either timed out or been cancelled, but for which we can't release the stream id because a + * request might still come from the server. + */ + int getOrphanedIds() { + int orphanedIds = 0; + DriverChannel[] snapshot = this.channels; + for (DriverChannel channel : snapshot) { + orphanedIds += channel.getOrphanedIds(); + } + return orphanedIds; + } + + int size() { + return this.channels.length; + } + + @NonNull + @Override + public Iterator iterator() { + return Iterators.forArray(this.channels); + } + + private static int indexOf(DriverChannel[] channels, DriverChannel key) { + for (int i = 0; i < channels.length; i++) { + if (channels[i] == key) { + return i; + } + } + return -1; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/ByteBufCompressor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/ByteBufCompressor.java new file mode 100644 index 00000000000..a8e4960ff49 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/ByteBufCompressor.java @@ -0,0 +1,61 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.protocol; + +import com.datastax.oss.protocol.internal.Compressor; +import io.netty.buffer.ByteBuf; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public abstract class ByteBufCompressor implements Compressor { + + @Override + public ByteBuf compress(ByteBuf uncompressed) { + return uncompressed.isDirect() ? compressDirect(uncompressed) : compressHeap(uncompressed); + } + + protected abstract ByteBuf compressDirect(ByteBuf input); + + protected abstract ByteBuf compressHeap(ByteBuf input); + + @Override + public ByteBuf decompress(ByteBuf compressed) { + return compressed.isDirect() ? decompressDirect(compressed) : decompressHeap(compressed); + } + + protected abstract ByteBuf decompressDirect(ByteBuf input); + + protected abstract ByteBuf decompressHeap(ByteBuf input); + + protected static ByteBuffer inputNioBuffer(ByteBuf buf) { + // Using internalNioBuffer(...) as we only hold the reference in this method and so can + // reduce Object allocations. + int index = buf.readerIndex(); + int len = buf.readableBytes(); + return buf.nioBufferCount() == 1 + ? buf.internalNioBuffer(index, len) + : buf.nioBuffer(index, len); + } + + protected static ByteBuffer outputNioBuffer(ByteBuf buf) { + int index = buf.writerIndex(); + int len = buf.writableBytes(); + return buf.nioBufferCount() == 1 + ? buf.internalNioBuffer(index, len) + : buf.nioBuffer(index, len); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/ByteBufPrimitiveCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/ByteBufPrimitiveCodec.java new file mode 100644 index 00000000000..73b92f479de --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/ByteBufPrimitiveCodec.java @@ -0,0 +1,233 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.protocol; + +import com.datastax.oss.protocol.internal.PrimitiveCodec; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.util.CharsetUtil; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class ByteBufPrimitiveCodec implements PrimitiveCodec { + + private final ByteBufAllocator allocator; + + public ByteBufPrimitiveCodec(ByteBufAllocator allocator) { + this.allocator = allocator; + } + + @Override + public ByteBuf allocate(int size) { + return allocator.ioBuffer(size, size); + } + + @Override + public void release(ByteBuf toRelease) { + toRelease.release(); + } + + @Override + public int sizeOf(ByteBuf toMeasure) { + return toMeasure.readableBytes(); + } + + @Override + public ByteBuf concat(ByteBuf left, ByteBuf right) { + if (!left.isReadable()) { + return right.duplicate(); + } else if (!right.isReadable()) { + return left.duplicate(); + } else { + CompositeByteBuf c = allocator.compositeBuffer(2); + c.addComponents(left, right); + // c.readerIndex() is 0, which is the first readable byte in left + c.writerIndex( + left.writerIndex() - left.readerIndex() + right.writerIndex() - right.readerIndex()); + return c; + } + } + + @Override + public byte readByte(ByteBuf source) { + return source.readByte(); + } + + @Override + public int readInt(ByteBuf source) { + return source.readInt(); + } + + @Override + public InetAddress readInetAddr(ByteBuf source) { + int length = readByte(source) & 0xFF; + byte[] bytes = new byte[length]; + source.readBytes(bytes); + return newInetAddress(bytes); + } + + @Override + public long readLong(ByteBuf source) { + return source.readLong(); + } + + @Override + public int readUnsignedShort(ByteBuf source) { + return source.readUnsignedShort(); + } + + @Override + public ByteBuffer readBytes(ByteBuf source) { + int length = readInt(source); + if (length < 0) return null; + ByteBuf slice = source.readSlice(length); + return ByteBuffer.wrap(readRawBytes(slice)); + } + + @Override + public byte[] readShortBytes(ByteBuf source) { + try { + int length = readUnsignedShort(source); + byte[] bytes = new byte[length]; + source.readBytes(bytes); + return bytes; + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException( + "Not enough bytes to read a byte array preceded by its 2 bytes length"); + } + } + + @Override + public String readString(ByteBuf source) { + int length = readUnsignedShort(source); + return readString(source, length); + } + + @Override + public String readLongString(ByteBuf source) { + int length = readInt(source); + return readString(source, length); + } + + @Override + public void writeByte(byte b, ByteBuf dest) { + dest.writeByte(b); + } + + @Override + public void writeInt(int i, ByteBuf dest) { + dest.writeInt(i); + } + + @Override + public void writeInetAddr(InetAddress inetAddr, ByteBuf dest) { + byte[] bytes = inetAddr.getAddress(); + writeByte((byte) bytes.length, dest); + dest.writeBytes(bytes); + } + + @Override + public void writeLong(long l, ByteBuf dest) { + dest.writeLong(l); + } + + @Override + public void writeUnsignedShort(int i, ByteBuf dest) { + dest.writeShort(i); + } + + @Override + public void writeString(String s, ByteBuf dest) { + byte[] bytes = s.getBytes(CharsetUtil.UTF_8); + writeUnsignedShort(bytes.length, dest); + dest.writeBytes(bytes); + } + + @Override + public void writeLongString(String s, ByteBuf dest) { + byte[] bytes = s.getBytes(CharsetUtil.UTF_8); + writeInt(bytes.length, dest); + dest.writeBytes(bytes); + } + + @Override + public void writeBytes(ByteBuffer bytes, ByteBuf dest) { + if (bytes == null) { + writeInt(-1, dest); + } else { + writeInt(bytes.remaining(), dest); + dest.writeBytes(bytes.duplicate()); + } + } + + @Override + public void writeBytes(byte[] bytes, ByteBuf dest) { + if (bytes == null) { + writeInt(-1, dest); + } else { + writeInt(bytes.length, dest); + dest.writeBytes(bytes); + } + } + + @Override + public void writeShortBytes(byte[] bytes, ByteBuf dest) { + writeUnsignedShort(bytes.length, dest); + dest.writeBytes(bytes); + } + + // Reads *all* readable bytes from a buffer and return them. + // If the buffer is backed by an array, this will return the underlying array directly, without + // copy. + private static byte[] readRawBytes(ByteBuf buffer) { + if (buffer.hasArray() && buffer.readableBytes() == buffer.array().length) { + // Move the readerIndex just so we consistently consume the input + buffer.readerIndex(buffer.writerIndex()); + return buffer.array(); + } + + // Otherwise, just read the bytes in a new array + byte[] bytes = new byte[buffer.readableBytes()]; + buffer.readBytes(bytes); + return bytes; + } + + private static String readString(ByteBuf source, int length) { + try { + String str = source.toString(source.readerIndex(), length, CharsetUtil.UTF_8); + source.readerIndex(source.readerIndex() + length); + return str; + } catch (IndexOutOfBoundsException e) { + throw new IllegalArgumentException( + "Not enough bytes to read an UTF-8 serialized string of size " + length, e); + } + } + + private InetAddress newInetAddress(byte[] bytes) { + try { + return InetAddress.getByAddress(bytes); + } catch (UnknownHostException e) { + // Per the Javadoc, the only way this can happen is if the length is illegal + throw new IllegalArgumentException( + String.format("Invalid address length: %d (%s)", bytes.length, Arrays.toString(bytes))); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/FrameDecoder.java b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/FrameDecoder.java new file mode 100644 index 00000000000..f9519e32035 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/FrameDecoder.java @@ -0,0 +1,114 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.protocol; + +import com.datastax.oss.driver.api.core.connection.FrameTooLongException; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.FrameCodec; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.Error; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.handler.codec.TooLongFrameException; +import java.util.Collections; +import net.jcip.annotations.NotThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@NotThreadSafe +public class FrameDecoder extends LengthFieldBasedFrameDecoder { + private static final Logger LOG = LoggerFactory.getLogger(FrameDecoder.class); + + // Where the length of the frame is located in the payload + private static final int LENGTH_FIELD_OFFSET = 5; + private static final int LENGTH_FIELD_LENGTH = 4; + + private final FrameCodec frameCodec; + private boolean isFirstResponse; + + public FrameDecoder(FrameCodec frameCodec, int maxFrameLengthInBytes) { + super(maxFrameLengthInBytes, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH, 0, 0, true); + this.frameCodec = frameCodec; + } + + @Override + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + int startIndex = in.readerIndex(); + if (isFirstResponse) { + isFirstResponse = false; + + // Must read at least the protocol v1/v2 header (see below) + if (in.readableBytes() < 8) { + return null; + } + // Special case for obsolete protocol versions (< v3): the length field is at a different + // position, so we can't delegate to super.decode() which would read the wrong length. + int protocolVersion = (int) in.getByte(startIndex) & 0b0111_1111; + if (protocolVersion < 3) { + int streamId = in.getByte(startIndex + 2); + int length = in.getInt(startIndex + 4); + // We don't need a full-blown decoder, just to signal the protocol error. So discard the + // incoming data and spoof a server-side protocol error. + if (in.readableBytes() < 8 + length) { + return null; // keep reading until we can discard the whole message at once + } else { + in.readerIndex(startIndex + 8 + length); + } + return Frame.forResponse( + protocolVersion, + streamId, + null, + Frame.NO_PAYLOAD, + Collections.emptyList(), + new Error( + ProtocolConstants.ErrorCode.PROTOCOL_ERROR, + "Invalid or unsupported protocol version")); + } + } + + try { + ByteBuf buffer = (ByteBuf) super.decode(ctx, in); + return (buffer == null) + ? null // did not receive whole frame yet, keep reading + : frameCodec.decode(buffer); + } catch (Exception e) { + // If decoding failed, try to read at least the stream id, so that the error can be + // propagated to the client request matching that id (otherwise we have to fail all + // pending requests on this channel) + int streamId; + try { + streamId = in.getShort(startIndex + 2); + } catch (Exception e1) { + // Should never happen, super.decode does not return a non-null buffer until the length + // field has been read, and the stream id comes before + Loggers.warnWithException(LOG, "Unexpected error while reading stream id", e1); + streamId = -1; + } + if (e instanceof TooLongFrameException) { + // Translate the Netty error to our own type + e = new FrameTooLongException(ctx.channel().remoteAddress(), e.getMessage()); + } + throw new FrameDecodingException(streamId, e); + } + } + + @Override + protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) { + return buffer.slice(index, length); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/FrameDecodingException.java b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/FrameDecodingException.java new file mode 100644 index 00000000000..ffa59651eec --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/FrameDecodingException.java @@ -0,0 +1,32 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.protocol; + +import io.netty.handler.codec.DecoderException; + +/** + * Wraps an error while decoding an incoming protocol frame. + * + *

This is only used internally, never exposed to the client. + */ +public class FrameDecodingException extends DecoderException { + public final int streamId; + + public FrameDecodingException(int streamId, Throwable cause) { + super("Error decoding frame for streamId " + streamId, cause); + this.streamId = streamId; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/FrameEncoder.java b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/FrameEncoder.java new file mode 100644 index 00000000000..d1c37f28654 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/FrameEncoder.java @@ -0,0 +1,52 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.protocol; + +import com.datastax.oss.driver.api.core.connection.FrameTooLongException; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.FrameCodec; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import java.util.List; +import net.jcip.annotations.ThreadSafe; + +@ChannelHandler.Sharable +@ThreadSafe +public class FrameEncoder extends MessageToMessageEncoder { + + private final FrameCodec frameCodec; + private final int maxFrameLength; + + public FrameEncoder(FrameCodec frameCodec, int maxFrameLength) { + super(Frame.class); + this.frameCodec = frameCodec; + this.maxFrameLength = maxFrameLength; + } + + @Override + protected void encode(ChannelHandlerContext ctx, Frame frame, List out) throws Exception { + ByteBuf buffer = frameCodec.encode(frame); + int actualLength = buffer.readableBytes(); + if (actualLength > maxFrameLength) { + throw new FrameTooLongException( + ctx.channel().remoteAddress(), + String.format("Outgoing frame length exceeds %d: %d", maxFrameLength, actualLength)); + } + out.add(buffer); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/Lz4Compressor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/Lz4Compressor.java new file mode 100644 index 00000000000..e1bce12fc11 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/Lz4Compressor.java @@ -0,0 +1,171 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.protocol; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import io.netty.buffer.ByteBuf; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; +import net.jpountz.lz4.LZ4Compressor; +import net.jpountz.lz4.LZ4Factory; +import net.jpountz.lz4.LZ4FastDecompressor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class Lz4Compressor extends ByteBufCompressor { + + private static final Logger LOG = LoggerFactory.getLogger(Lz4Compressor.class); + + private final LZ4Compressor compressor; + private final LZ4FastDecompressor decompressor; + + public Lz4Compressor(DriverContext context) { + try { + LZ4Factory lz4Factory = LZ4Factory.fastestInstance(); + LOG.info("[{}] Using {}", context.getSessionName(), lz4Factory.toString()); + this.compressor = lz4Factory.fastCompressor(); + this.decompressor = lz4Factory.fastDecompressor(); + } catch (NoClassDefFoundError e) { + throw new IllegalStateException( + "Error initializing compressor, make sure that the LZ4 library is in the classpath " + + "(the driver declares it as an optional dependency, " + + "so you need to declare it explicitly)", + e); + } + } + + @Override + public String algorithm() { + return "lz4"; + } + + @Override + protected ByteBuf compressDirect(ByteBuf input) { + int maxCompressedLength = compressor.maxCompressedLength(input.readableBytes()); + // If the input is direct we will allocate a direct output buffer as well as this will allow us + // to use LZ4Compressor.compress and so eliminate memory copies. + ByteBuf output = input.alloc().directBuffer(4 + maxCompressedLength); + try { + ByteBuffer in = inputNioBuffer(input); + // Increase reader index. + input.readerIndex(input.writerIndex()); + + output.writeInt(in.remaining()); + + ByteBuffer out = outputNioBuffer(output); + int written = + compressor.compress( + in, in.position(), in.remaining(), out, out.position(), out.remaining()); + // Set the writer index so the amount of written bytes is reflected + output.writerIndex(output.writerIndex() + written); + } catch (Exception e) { + // release output buffer so we not leak and rethrow exception. + output.release(); + throw e; + } + return output; + } + + @Override + protected ByteBuf compressHeap(ByteBuf input) { + int maxCompressedLength = compressor.maxCompressedLength(input.readableBytes()); + + // Not a direct buffer so use byte arrays... + int inOffset = input.arrayOffset() + input.readerIndex(); + byte[] in = input.array(); + int len = input.readableBytes(); + // Increase reader index. + input.readerIndex(input.writerIndex()); + + // Allocate a heap buffer from the ByteBufAllocator as we may use a PooledByteBufAllocator and + // so can eliminate the overhead of allocate a new byte[]. + ByteBuf output = input.alloc().heapBuffer(4 + maxCompressedLength); + try { + output.writeInt(len); + // calculate the correct offset. + int offset = output.arrayOffset() + output.writerIndex(); + byte[] out = output.array(); + int written = compressor.compress(in, inOffset, len, out, offset); + + // Set the writer index so the amount of written bytes is reflected + output.writerIndex(output.writerIndex() + written); + } catch (Exception e) { + // release output buffer so we not leak and rethrow exception. + output.release(); + throw e; + } + return output; + } + + @Override + protected ByteBuf decompressDirect(ByteBuf input) { + // If the input is direct we will allocate a direct output buffer as well as this will allow us + // to use LZ4Compressor.decompress and so eliminate memory copies. + int readable = input.readableBytes(); + int uncompressedLength = input.readInt(); + ByteBuffer in = inputNioBuffer(input); + // Increase reader index. + input.readerIndex(input.writerIndex()); + ByteBuf output = input.alloc().directBuffer(uncompressedLength); + try { + ByteBuffer out = outputNioBuffer(output); + int read = decompressor.decompress(in, in.position(), out, out.position(), out.remaining()); + if (read != readable - 4) { + throw new IllegalArgumentException("Compressed lengths mismatch"); + } + + // Set the writer index so the amount of written bytes is reflected + output.writerIndex(output.writerIndex() + uncompressedLength); + } catch (Exception e) { + // release output buffer so we not leak and rethrow exception. + output.release(); + throw e; + } + return output; + } + + @Override + protected ByteBuf decompressHeap(ByteBuf input) { + // Not a direct buffer so use byte arrays... + byte[] in = input.array(); + int len = input.readableBytes(); + int uncompressedLength = input.readInt(); + int inOffset = input.arrayOffset() + input.readerIndex(); + // Increase reader index. + input.readerIndex(input.writerIndex()); + + // Allocate a heap buffer from the ByteBufAllocator as we may use a PooledByteBufAllocator and + // so can eliminate the overhead of allocate a new byte[]. + ByteBuf output = input.alloc().heapBuffer(uncompressedLength); + try { + int offset = output.arrayOffset() + output.writerIndex(); + byte out[] = output.array(); + int read = decompressor.decompress(in, inOffset, out, offset, uncompressedLength); + if (read != len - 4) { + throw new IllegalArgumentException("Compressed lengths mismatch"); + } + + // Set the writer index so the amount of written bytes is reflected + output.writerIndex(output.writerIndex() + uncompressedLength); + } catch (Exception e) { + // release output buffer so we not leak and rethrow exception. + output.release(); + throw e; + } + return output; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/SnappyCompressor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/SnappyCompressor.java new file mode 100644 index 00000000000..3b50220ef95 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/SnappyCompressor.java @@ -0,0 +1,160 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.protocol; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import io.netty.buffer.ByteBuf; +import java.io.IOException; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; +import org.xerial.snappy.Snappy; + +@ThreadSafe +public class SnappyCompressor extends ByteBufCompressor { + + public SnappyCompressor(@SuppressWarnings("unused") DriverContext context) { + try { + Snappy.getNativeLibraryVersion(); + } catch (NoClassDefFoundError e) { + throw new IllegalStateException( + "Error initializing compressor, make sure that the Snappy library is in the classpath " + + "(the driver declares it as an optional dependency, " + + "so you need to declare it explicitly)", + e); + } + } + + @Override + public String algorithm() { + return "snappy"; + } + + @Override + protected ByteBuf compressDirect(ByteBuf input) { + int maxCompressedLength = Snappy.maxCompressedLength(input.readableBytes()); + // If the input is direct we will allocate a direct output buffer as well as this will allow us + // to use Snappy.compress(ByteBuffer, ByteBuffer) and so eliminate memory copies. + ByteBuf output = input.alloc().directBuffer(maxCompressedLength); + try { + ByteBuffer in = inputNioBuffer(input); + // Increase reader index. + input.readerIndex(input.writerIndex()); + + ByteBuffer out = outputNioBuffer(output); + int written = Snappy.compress(in, out); + // Set the writer index so the amount of written bytes is reflected + output.writerIndex(output.writerIndex() + written); + return output; + } catch (IOException e) { + // release output buffer so we not leak and rethrow exception. + output.release(); + throw new RuntimeException(e); + } + } + + @Override + protected ByteBuf compressHeap(ByteBuf input) { + int maxCompressedLength = Snappy.maxCompressedLength(input.readableBytes()); + int inOffset = input.arrayOffset() + input.readerIndex(); + byte[] in = input.array(); + int len = input.readableBytes(); + // Increase reader index. + input.readerIndex(input.writerIndex()); + + // Allocate a heap buffer from the ByteBufAllocator as we may use a PooledByteBufAllocator and + // so can eliminate the overhead of allocate a new byte[]. + ByteBuf output = input.alloc().heapBuffer(maxCompressedLength); + try { + // Calculate the correct offset. + int offset = output.arrayOffset() + output.writerIndex(); + byte[] out = output.array(); + int written = Snappy.compress(in, inOffset, len, out, offset); + + // Increase the writerIndex with the written bytes. + output.writerIndex(output.writerIndex() + written); + return output; + } catch (IOException e) { + // release output buffer so we not leak and rethrow exception. + output.release(); + throw new RuntimeException(e); + } + } + + @Override + protected ByteBuf decompressDirect(ByteBuf input) { + ByteBuffer in = inputNioBuffer(input); + // Increase reader index. + input.readerIndex(input.writerIndex()); + + ByteBuf output = null; + try { + if (!Snappy.isValidCompressedBuffer(in)) { + throw new IllegalArgumentException( + "Provided frame does not appear to be Snappy compressed"); + } + // If the input is direct we will allocate a direct output buffer as well as this will allow + // us to use Snappy.compress(ByteBuffer, ByteBuffer) and so eliminate memory copies. + output = input.alloc().directBuffer(Snappy.uncompressedLength(in)); + ByteBuffer out = outputNioBuffer(output); + + int size = Snappy.uncompress(in, out); + // Set the writer index so the amount of written bytes is reflected + output.writerIndex(output.writerIndex() + size); + return output; + } catch (IOException e) { + // release output buffer so we not leak and rethrow exception. + if (output != null) { + output.release(); + } + throw new RuntimeException(e); + } + } + + @Override + protected ByteBuf decompressHeap(ByteBuf input) throws RuntimeException { + // Not a direct buffer so use byte arrays... + int inOffset = input.arrayOffset() + input.readerIndex(); + byte[] in = input.array(); + int len = input.readableBytes(); + // Increase reader index. + input.readerIndex(input.writerIndex()); + + ByteBuf output = null; + try { + if (!Snappy.isValidCompressedBuffer(in, inOffset, len)) { + throw new IllegalArgumentException( + "Provided frame does not appear to be Snappy compressed"); + } + // Allocate a heap buffer from the ByteBufAllocator as we may use a PooledByteBufAllocator and + // so can eliminate the overhead of allocate a new byte[]. + output = input.alloc().heapBuffer(Snappy.uncompressedLength(in, inOffset, len)); + // Calculate the correct offset. + int offset = output.arrayOffset() + output.writerIndex(); + byte[] out = output.array(); + int written = Snappy.uncompress(in, inOffset, len, out, offset); + + // Increase the writerIndex with the written bytes. + output.writerIndex(output.writerIndex() + written); + return output; + } catch (IOException e) { + // release output buffer so we not leak and rethrow exception. + if (output != null) { + output.release(); + } + throw new RuntimeException(e); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/package-info.java b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/package-info.java new file mode 100644 index 00000000000..a77aede1ecf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/protocol/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** Specialization of the native protocol layer for the driver, based on Netty. */ +package com.datastax.oss.driver.internal.core.protocol; diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/retry/DefaultRetryPolicy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/retry/DefaultRetryPolicy.java new file mode 100644 index 00000000000..c15cfb41baa --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/retry/DefaultRetryPolicy.java @@ -0,0 +1,233 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.retry; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.connection.ClosedConnectionException; +import com.datastax.oss.driver.api.core.connection.HeartbeatException; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.retry.RetryDecision; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.servererrors.CoordinatorException; +import com.datastax.oss.driver.api.core.servererrors.DefaultWriteType; +import com.datastax.oss.driver.api.core.servererrors.ReadFailureException; +import com.datastax.oss.driver.api.core.servererrors.WriteFailureException; +import com.datastax.oss.driver.api.core.servererrors.WriteType; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The default retry policy. + * + *

This is a very conservative implementation: it triggers a maximum of one retry per request, + * and only in cases that have a high chance of success (see the method javadocs for detailed + * explanations of each case). + * + *

To activate this policy, modify the {@code advanced.retry-policy} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.retry-policy {
+ *     class = DefaultRetryPolicy
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class DefaultRetryPolicy implements RetryPolicy { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultRetryPolicy.class); + + @VisibleForTesting + static final String RETRYING_ON_READ_TIMEOUT = + "[{}] Retrying on read timeout on same host (consistency: {}, required responses: {}, " + + "received responses: {}, data retrieved: {}, retries: {})"; + + @VisibleForTesting + static final String RETRYING_ON_WRITE_TIMEOUT = + "[{}] Retrying on write timeout on same host (consistency: {}, write type: {}, " + + "required acknowledgments: {}, received acknowledgments: {}, retries: {})"; + + @VisibleForTesting + static final String RETRYING_ON_UNAVAILABLE = + "[{}] Retrying on unavailable exception on next host (consistency: {}, " + + "required replica: {}, alive replica: {}, retries: {})"; + + @VisibleForTesting + static final String RETRYING_ON_ABORTED = + "[{}] Retrying on aborted request on next host (retries: {})"; + + @VisibleForTesting + static final String RETRYING_ON_ERROR = "[{}] Retrying on node error on next host (retries: {})"; + + private final String logPrefix; + + public DefaultRetryPolicy( + @SuppressWarnings("unused") DriverContext context, + @SuppressWarnings("unused") String profileName) { + this.logPrefix = (context != null ? context.getSessionName() : null) + "|" + profileName; + } + + /** + * {@inheritDoc} + * + *

This implementation triggers a maximum of one retry (to the same node), and only if enough + * replicas had responded to the read request but data was not retrieved amongst those. That + * usually means that enough replicas are alive to satisfy the consistency, but the coordinator + * picked a dead one for data retrieval, not having detected that replica as dead yet. The + * reasoning is that by the time we get the timeout, the dead replica will likely have been + * detected as dead and the retry has a high chance of success. + * + *

Otherwise, the exception is rethrown. + */ + @Override + public RetryDecision onReadTimeout( + @NonNull Request request, + @NonNull ConsistencyLevel cl, + int blockFor, + int received, + boolean dataPresent, + int retryCount) { + + RetryDecision decision = + (retryCount == 0 && received >= blockFor && !dataPresent) + ? RetryDecision.RETRY_SAME + : RetryDecision.RETHROW; + + if (decision == RetryDecision.RETRY_SAME && LOG.isTraceEnabled()) { + LOG.trace(RETRYING_ON_READ_TIMEOUT, logPrefix, cl, blockFor, received, false, retryCount); + } + + return decision; + } + + /** + * {@inheritDoc} + * + *

This implementation triggers a maximum of one retry (to the same node), and only for a + * {@code WriteType.BATCH_LOG} write. The reasoning is that the coordinator tries to write the + * distributed batch log against a small subset of nodes in the local datacenter; a timeout + * usually means that none of these nodes were alive but the coordinator hadn't detected them as + * dead yet. By the time we get the timeout, the dead nodes will likely have been detected as + * dead, and the retry has thus a high chance of success. + * + *

Otherwise, the exception is rethrown. + */ + @Override + public RetryDecision onWriteTimeout( + @NonNull Request request, + @NonNull ConsistencyLevel cl, + @NonNull WriteType writeType, + int blockFor, + int received, + int retryCount) { + + RetryDecision decision = + (retryCount == 0 && writeType == DefaultWriteType.BATCH_LOG) + ? RetryDecision.RETRY_SAME + : RetryDecision.RETHROW; + + if (decision == RetryDecision.RETRY_SAME && LOG.isTraceEnabled()) { + LOG.trace( + RETRYING_ON_WRITE_TIMEOUT, logPrefix, cl, writeType, blockFor, received, retryCount); + } + return decision; + } + + /** + * {@inheritDoc} + * + *

This implementation triggers a maximum of one retry, to the next node in the query plan. The + * rationale is that the first coordinator might have been network-isolated from all other nodes + * (thinking they're down), but still able to communicate with the client; in that case, retrying + * on the same host has almost no chance of success, but moving to the next host might solve the + * issue. + * + *

Otherwise, the exception is rethrown. + */ + @Override + public RetryDecision onUnavailable( + @NonNull Request request, + @NonNull ConsistencyLevel cl, + int required, + int alive, + int retryCount) { + + RetryDecision decision = (retryCount == 0) ? RetryDecision.RETRY_NEXT : RetryDecision.RETHROW; + + if (decision == RetryDecision.RETRY_NEXT && LOG.isTraceEnabled()) { + LOG.trace(RETRYING_ON_UNAVAILABLE, logPrefix, cl, required, alive, retryCount); + } + + return decision; + } + + /** + * {@inheritDoc} + * + *

This implementation retries on the next node if the connection was closed, and rethrows + * (assuming a driver bug) in all other cases. + */ + @Override + public RetryDecision onRequestAborted( + @NonNull Request request, @NonNull Throwable error, int retryCount) { + + RetryDecision decision = + (error instanceof ClosedConnectionException || error instanceof HeartbeatException) + ? RetryDecision.RETRY_NEXT + : RetryDecision.RETHROW; + + if (decision == RetryDecision.RETRY_NEXT && LOG.isTraceEnabled()) { + LOG.trace(RETRYING_ON_ABORTED, logPrefix, retryCount, error); + } + + return decision; + } + + /** + * {@inheritDoc} + * + *

This implementation rethrows read and write failures, and retries other errors on the next + * node. + */ + @Override + public RetryDecision onErrorResponse( + @NonNull Request request, @NonNull CoordinatorException error, int retryCount) { + + RetryDecision decision = + (error instanceof ReadFailureException || error instanceof WriteFailureException) + ? RetryDecision.RETHROW + : RetryDecision.RETRY_NEXT; + + if (decision == RetryDecision.RETRY_NEXT && LOG.isTraceEnabled()) { + LOG.trace(RETRYING_ON_ERROR, logPrefix, retryCount, error); + } + + return decision; + } + + @Override + public void close() { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/servererrors/DefaultWriteTypeRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/servererrors/DefaultWriteTypeRegistry.java new file mode 100644 index 00000000000..598db8fbbbe --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/servererrors/DefaultWriteTypeRegistry.java @@ -0,0 +1,38 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.servererrors; + +import com.datastax.oss.driver.api.core.servererrors.DefaultWriteType; +import com.datastax.oss.driver.api.core.servererrors.WriteType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DefaultWriteTypeRegistry implements WriteTypeRegistry { + + private static final ImmutableList values = + ImmutableList.builder().add(DefaultWriteType.values()).build(); + + @Override + public WriteType fromName(String name) { + return DefaultWriteType.valueOf(name); + } + + @Override + public ImmutableList getValues() { + return values; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/servererrors/WriteTypeRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/servererrors/WriteTypeRegistry.java new file mode 100644 index 00000000000..1634b3066bf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/servererrors/WriteTypeRegistry.java @@ -0,0 +1,25 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.servererrors; + +import com.datastax.oss.driver.api.core.servererrors.WriteType; + +public interface WriteTypeRegistry { + WriteType fromName(String name); + + /** @return all the values known to this driver instance. */ + Iterable getValues(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/DefaultSession.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/DefaultSession.java new file mode 100644 index 00000000000..d29823f3d03 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/DefaultSession.java @@ -0,0 +1,588 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import com.datastax.oss.driver.api.core.AsyncAutoCloseable; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.metrics.Metrics; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.LifecycleListener; +import com.datastax.oss.driver.internal.core.control.ControlConnection; +import com.datastax.oss.driver.internal.core.metadata.MetadataManager; +import com.datastax.oss.driver.internal.core.metadata.NodeStateEvent; +import com.datastax.oss.driver.internal.core.metadata.NodeStateManager; +import com.datastax.oss.driver.internal.core.metrics.SessionMetricUpdater; +import com.datastax.oss.driver.internal.core.pool.ChannelPool; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import io.netty.util.concurrent.EventExecutor; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Supplier; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The session implementation. + * + *

It maintains a {@link ChannelPool} to each node that the {@link LoadBalancingPolicy} set to a + * non-ignored distance. It listens for distance events and node state events, in order to adjust + * the pools accordingly. + * + *

It executes requests by: + * + *

    + *
  • picking the appropriate processor to convert the request into a protocol message. + *
  • getting a query plan from the load balancing policy + *
  • trying to send the message on each pool, in the order of the query plan + *
+ */ +@ThreadSafe +public class DefaultSession implements CqlSession { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultSession.class); + + public static CompletionStage init( + InternalDriverContext context, Set contactPoints, CqlIdentifier keyspace) { + return new DefaultSession(context, contactPoints).init(keyspace); + } + + private final InternalDriverContext context; + private final EventExecutor adminExecutor; + private final String logPrefix; + private final SingleThreaded singleThreaded; + private final MetadataManager metadataManager; + private final RequestProcessorRegistry processorRegistry; + private final PoolManager poolManager; + private final SessionMetricUpdater metricUpdater; + + private DefaultSession(InternalDriverContext context, Set contactPoints) { + LOG.debug("Creating new session {}", context.getSessionName()); + this.logPrefix = context.getSessionName(); + this.adminExecutor = context.getNettyOptions().adminEventExecutorGroup().next(); + try { + this.context = context; + this.singleThreaded = new SingleThreaded(context, contactPoints); + this.metadataManager = context.getMetadataManager(); + this.processorRegistry = context.getRequestProcessorRegistry(); + this.poolManager = context.getPoolManager(); + this.metricUpdater = context.getMetricsFactory().getSessionUpdater(); + } catch (Throwable t) { + // Rethrow but make sure we release any resources allocated by Netty. At this stage there are + // no scheduled tasks on the event loops so getNow() won't block. + try { + context.getNettyOptions().onClose().getNow(); + } catch (Throwable suppressed) { + Loggers.warnWithException( + LOG, + "[{}] Error while closing NettyOptions " + + "(suppressed because we're already handling an init failure)", + logPrefix, + suppressed); + } + throw t; + } + } + + private CompletionStage init(CqlIdentifier keyspace) { + RunOrSchedule.on(adminExecutor, () -> singleThreaded.init(keyspace)); + return singleThreaded.initFuture; + } + + @NonNull + @Override + public String getName() { + return context.getSessionName(); + } + + @NonNull + @Override + public Metadata getMetadata() { + return metadataManager.getMetadata(); + } + + @Override + public boolean isSchemaMetadataEnabled() { + return metadataManager.isSchemaEnabled(); + } + + @NonNull + @Override + public CompletionStage setSchemaMetadataEnabled(@Nullable Boolean newValue) { + return metadataManager.setSchemaEnabled(newValue); + } + + @NonNull + @Override + public CompletionStage refreshSchemaAsync() { + return metadataManager.refreshSchema(null, true, true); + } + + @NonNull + @Override + public CompletionStage checkSchemaAgreementAsync() { + return context.getTopologyMonitor().checkSchemaAgreement(); + } + + @NonNull + @Override + public DriverContext getContext() { + return context; + } + + @NonNull + @Override + public Optional getKeyspace() { + return Optional.ofNullable(poolManager.getKeyspace()); + } + + @NonNull + @Override + public Optional getMetrics() { + return context.getMetricsFactory().getMetrics(); + } + + /** + * INTERNAL USE ONLY -- switches the session to a new keyspace. + * + *

This is called by the driver when a {@code USE} query is successfully executed through the + * session. Calling it from anywhere else is highly discouraged, as an invalid keyspace would + * wreak havoc (close all connections and make the session unusable). + */ + @NonNull + public CompletionStage setKeyspace(@NonNull CqlIdentifier newKeyspace) { + return poolManager.setKeyspace(newKeyspace); + } + + @NonNull + public Map getPools() { + return poolManager.getPools(); + } + + @Nullable + @Override + public ResultT execute( + @NonNull RequestT request, @NonNull GenericType resultType) { + RequestProcessor processor = + processorRegistry.processorFor(request, resultType); + return isClosed() + ? processor.newFailure(new IllegalStateException("Session is closed")) + : processor.process(request, this, context, logPrefix); + } + + @Nullable + public DriverChannel getChannel(@NonNull Node node, @NonNull String logPrefix) { + ChannelPool pool = poolManager.getPools().get(node); + if (pool == null) { + LOG.trace("[{}] No pool to {}, skipping", logPrefix, node); + return null; + } else { + DriverChannel channel = pool.next(); + if (channel == null) { + LOG.trace("[{}] Pool returned no channel for {}, skipping", logPrefix, node); + return null; + } else if (channel.closeFuture().isDone()) { + LOG.trace("[{}] Pool returned closed connection to {}, skipping", logPrefix, node); + return null; + } else { + return channel; + } + } + } + + @NonNull + public ConcurrentMap getRepreparePayloads() { + return poolManager.getRepreparePayloads(); + } + + @NonNull + public SessionMetricUpdater getMetricUpdater() { + return metricUpdater; + } + + @NonNull + @Override + public CompletionStage closeFuture() { + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage closeAsync() { + RunOrSchedule.on(adminExecutor, singleThreaded::close); + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage forceCloseAsync() { + RunOrSchedule.on(adminExecutor, singleThreaded::forceClose); + return singleThreaded.closeFuture; + } + + private class SingleThreaded { + + private final InternalDriverContext context; + private final Set initialContactPoints; + private final NodeStateManager nodeStateManager; + private final CompletableFuture initFuture = new CompletableFuture<>(); + private boolean initWasCalled; + private final CompletableFuture closeFuture = new CompletableFuture<>(); + private boolean closeWasCalled; + private boolean forceCloseWasCalled; + + private SingleThreaded(InternalDriverContext context, Set contactPoints) { + this.context = context; + this.nodeStateManager = new NodeStateManager(context); + this.initialContactPoints = contactPoints; + new SchemaListenerNotifier( + context.getSchemaChangeListener(), context.getEventBus(), adminExecutor); + context + .getEventBus() + .register( + NodeStateEvent.class, RunOrSchedule.on(adminExecutor, this::onNodeStateChanged)); + CompletableFutures.propagateCancellation( + this.initFuture, context.getTopologyMonitor().initFuture()); + } + + private void init(CqlIdentifier keyspace) { + assert adminExecutor.inEventLoop(); + if (initWasCalled) { + return; + } + initWasCalled = true; + LOG.debug("[{}] Starting initialization", logPrefix); + + // Eagerly fetch user-facing policies right now, no need to start opening connections if + // something is wrong in the configuration. + try { + context.getLoadBalancingPolicies(); + context.getRetryPolicies(); + context.getSpeculativeExecutionPolicies(); + context.getReconnectionPolicy(); + context.getAddressTranslator(); + context.getNodeStateListener(); + context.getSchemaChangeListener(); + context.getRequestTracker(); + context.getRequestThrottler(); + context.getAuthProvider(); + context.getSslHandlerFactory(); + context.getTimestampGenerator(); + } catch (Throwable error) { + RunOrSchedule.on(adminExecutor, this::closePolicies); + context + .getNettyOptions() + .onClose() + .addListener( + f -> { + if (!f.isSuccess()) { + Loggers.warnWithException( + LOG, + "[{}] Error while closing NettyOptions " + + "(suppressed because we're already handling an init failure)", + logPrefix, + f.cause()); + } + initFuture.completeExceptionally(error); + }); + return; + } + + MetadataManager metadataManager = context.getMetadataManager(); + metadataManager.addContactPoints(initialContactPoints); + context + .getTopologyMonitor() + .init() + .thenCompose(v -> metadataManager.refreshNodes()) + .thenAccept(v -> afterInitialNodeListRefresh(keyspace)) + .exceptionally( + error -> { + initFuture.completeExceptionally(error); + RunOrSchedule.on(adminExecutor, this::close); + return null; + }); + } + + private void afterInitialNodeListRefresh(CqlIdentifier keyspace) { + try { + boolean protocolWasForced = + context.getConfig().getDefaultProfile().isDefined(DefaultDriverOption.PROTOCOL_VERSION); + boolean needSchemaRefresh = true; + if (!protocolWasForced) { + ProtocolVersion currentVersion = context.getProtocolVersion(); + ProtocolVersion bestVersion = + context + .getProtocolVersionRegistry() + .highestCommon(metadataManager.getMetadata().getNodes().values()); + if (!currentVersion.equals(bestVersion)) { + LOG.info( + "[{}] Negotiated protocol version {} for the initial contact point, " + + "but other nodes only support {}, downgrading", + logPrefix, + currentVersion, + bestVersion); + context.getChannelFactory().setProtocolVersion(bestVersion); + ControlConnection controlConnection = context.getControlConnection(); + // Might not have initialized yet if there is a custom TopologyMonitor + if (controlConnection.isInit()) { + controlConnection.reconnectNow(); + // Reconnection already triggers a full schema refresh + needSchemaRefresh = false; + } + } + } + if (needSchemaRefresh) { + metadataManager.refreshSchema(null, false, true); + } + metadataManager + .firstSchemaRefreshFuture() + .thenAccept(v -> afterInitialSchemaRefresh(keyspace)); + + } catch (Throwable throwable) { + initFuture.completeExceptionally(throwable); + } + } + + private void afterInitialSchemaRefresh(CqlIdentifier keyspace) { + try { + nodeStateManager.markInitialized(); + context.getLoadBalancingPolicyWrapper().init(); + context.getConfigLoader().onDriverInit(context); + LOG.debug("[{}] Initialization complete, ready", logPrefix); + poolManager + .init(keyspace) + .whenComplete( + (v, error) -> { + if (error != null) { + initFuture.completeExceptionally(error); + } else { + initFuture.complete(DefaultSession.this); + notifyLifecycleListeners(); + } + }); + } catch (Throwable throwable) { + forceCloseAsync() + .whenComplete( + (v, error) -> { + initFuture.completeExceptionally(throwable); + }); + } + } + + private void notifyLifecycleListeners() { + for (LifecycleListener lifecycleListener : context.getLifecycleListeners()) { + try { + lifecycleListener.onSessionReady(); + } catch (Throwable t) { + Loggers.warnWithException( + LOG, + "[{}] Error while notifying {} of session ready", + logPrefix, + lifecycleListener, + t); + } + } + } + + private void onNodeStateChanged(NodeStateEvent event) { + assert adminExecutor.inEventLoop(); + if (event.newState == null) { + context.getNodeStateListener().onRemove(event.node); + } else if (event.oldState == null && event.newState == NodeState.UNKNOWN) { + context.getNodeStateListener().onAdd(event.node); + } else if (event.newState == NodeState.UP) { + context.getNodeStateListener().onUp(event.node); + } else if (event.newState == NodeState.DOWN || event.newState == NodeState.FORCED_DOWN) { + context.getNodeStateListener().onDown(event.node); + } + } + + private void close() { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + return; + } + closeWasCalled = true; + LOG.debug("[{}] Starting shutdown", logPrefix); + + closePolicies(); + + List> childrenCloseStages = new ArrayList<>(); + for (AsyncAutoCloseable closeable : internalComponentsToClose()) { + childrenCloseStages.add(closeable.closeAsync()); + } + CompletableFutures.whenAllDone( + childrenCloseStages, () -> onChildrenClosed(childrenCloseStages), adminExecutor); + } + + private void forceClose() { + assert adminExecutor.inEventLoop(); + if (forceCloseWasCalled) { + return; + } + forceCloseWasCalled = true; + LOG.debug( + "[{}] Starting forced shutdown (was {}closed before)", + logPrefix, + (closeWasCalled ? "" : "not ")); + + if (closeWasCalled) { + // onChildrenClosed has already been scheduled + for (AsyncAutoCloseable closeable : internalComponentsToClose()) { + closeable.forceCloseAsync(); + } + } else { + closePolicies(); + List> childrenCloseStages = new ArrayList<>(); + for (AsyncAutoCloseable closeable : internalComponentsToClose()) { + childrenCloseStages.add(closeable.forceCloseAsync()); + } + CompletableFutures.whenAllDone( + childrenCloseStages, () -> onChildrenClosed(childrenCloseStages), adminExecutor); + } + } + + private void onChildrenClosed(List> childrenCloseStages) { + assert adminExecutor.inEventLoop(); + for (CompletionStage stage : childrenCloseStages) { + warnIfFailed(stage); + } + context + .getNettyOptions() + .onClose() + .addListener( + f -> { + if (!f.isSuccess()) { + closeFuture.completeExceptionally(f.cause()); + } else { + LOG.debug("[{}] Shutdown complete", logPrefix); + closeFuture.complete(null); + } + }); + } + + private void warnIfFailed(CompletionStage stage) { + CompletableFuture future = stage.toCompletableFuture(); + assert future.isDone(); + if (future.isCompletedExceptionally()) { + Loggers.warnWithException( + LOG, + "[{}] Unexpected error while closing", + logPrefix, + CompletableFutures.getFailed(future)); + } + } + + private void closePolicies() { + // This is a bit tricky: we might be closing the session because of an initialization error. + // This error might have been triggered by a policy failing to initialize. If we try to access + // the policy here to close it, it will fail again. So make sure we ignore that error and + // proceed to close the other policies. + List policies = new ArrayList<>(); + for (Supplier supplier : + ImmutableList.>of( + context::getReconnectionPolicy, + context::getLoadBalancingPolicyWrapper, + context::getAddressTranslator, + context::getConfigLoader, + context::getNodeStateListener, + context::getSchemaChangeListener, + context::getRequestTracker, + context::getRequestThrottler, + context::getTimestampGenerator)) { + try { + policies.add(supplier.get()); + } catch (Throwable t) { + // Assume the policy had failed to initialize, and we don't need to close it => ignore + } + } + try { + context.getAuthProvider().ifPresent(policies::add); + } catch (Throwable t) { + // ignore + } + try { + context.getSslHandlerFactory().ifPresent(policies::add); + } catch (Throwable t) { + // ignore + } + try { + policies.addAll(context.getRetryPolicies().values()); + } catch (Throwable t) { + // ignore + } + try { + policies.addAll(context.getSpeculativeExecutionPolicies().values()); + } catch (Throwable t) { + // ignore + } + policies.addAll(context.getLifecycleListeners()); + + // Finally we have a list of all the policies that initialized successfully, close them: + for (AutoCloseable policy : policies) { + try { + policy.close(); + } catch (Throwable t) { + Loggers.warnWithException(LOG, "[{}] Error while closing {}", logPrefix, policy, t); + } + } + } + + private List internalComponentsToClose() { + ImmutableList.Builder components = + ImmutableList.builder() + .add(poolManager, nodeStateManager, metadataManager); + + // Same as closePolicies(): make sure we don't trigger errors by accessing context components + // that had failed to initialize: + try { + components.add(context.getTopologyMonitor()); + } catch (Throwable t) { + // ignore + } + try { + components.add(context.getControlConnection()); + } catch (Throwable t) { + // ignore + } + return components.build(); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/PoolManager.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/PoolManager.java new file mode 100644 index 00000000000..610669e965b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/PoolManager.java @@ -0,0 +1,522 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import com.datastax.oss.driver.api.core.AsyncAutoCloseable; +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.InvalidKeyspaceException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.metadata.DistanceEvent; +import com.datastax.oss.driver.internal.core.metadata.NodeStateEvent; +import com.datastax.oss.driver.internal.core.metadata.TopologyEvent; +import com.datastax.oss.driver.internal.core.pool.ChannelPool; +import com.datastax.oss.driver.internal.core.pool.ChannelPoolFactory; +import com.datastax.oss.driver.internal.core.util.Loggers; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.internal.core.util.concurrent.ReplayingEventFilter; +import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule; +import com.datastax.oss.driver.internal.core.util.concurrent.UncaughtExceptions; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.driver.shaded.guava.common.collect.MapMaker; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.util.concurrent.EventExecutor; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.WeakHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Maintains the connection pools of a session. + * + *

Logically this belongs to {@link DefaultSession}, but it's extracted here in order to be + * accessible from the context (notably for metrics). + */ +@ThreadSafe +public class PoolManager implements AsyncAutoCloseable { + + private static final Logger LOG = LoggerFactory.getLogger(PoolManager.class); + + // This is read concurrently, but only updated from adminExecutor + private volatile CqlIdentifier keyspace; + + private final ConcurrentMap pools = + new ConcurrentHashMap<>( + 16, + 0.75f, + // the map will only be updated from adminExecutor + 1); + + // The raw data to reprepare requests on the fly, if we hit a node that doesn't have them in + // its cache. + // This is raw protocol-level data, as opposed to the actual instances returned to the client + // (e.g. DefaultPreparedStatement) which are handled at the protocol level (e.g. + // CqlPrepareAsyncProcessor). We keep the two separate to avoid introducing a dependency from the + // session to a particular processor implementation. + private ConcurrentMap repreparePayloads = + new MapMaker().weakValues().makeMap(); + + private final String logPrefix; + private final EventExecutor adminExecutor; + private final DriverExecutionProfile config; + private final SingleThreaded singleThreaded; + + public PoolManager(InternalDriverContext context) { + this.logPrefix = context.getSessionName(); + this.adminExecutor = context.getNettyOptions().adminEventExecutorGroup().next(); + this.config = context.getConfig().getDefaultProfile(); + this.singleThreaded = new SingleThreaded(context); + } + + public CompletionStage init(CqlIdentifier keyspace) { + RunOrSchedule.on(adminExecutor, () -> singleThreaded.init(keyspace)); + return singleThreaded.initFuture; + } + + public CqlIdentifier getKeyspace() { + return keyspace; + } + + public CompletionStage setKeyspace(CqlIdentifier newKeyspace) { + CqlIdentifier oldKeyspace = this.keyspace; + if (Objects.equals(oldKeyspace, newKeyspace)) { + return CompletableFuture.completedFuture(null); + } + if (config.getBoolean(DefaultDriverOption.REQUEST_WARN_IF_SET_KEYSPACE)) { + LOG.warn( + "[{}] Detected a keyspace change at runtime ({} => {}). " + + "This is an anti-pattern that should be avoided in production " + + "(see '{}' in the configuration).", + logPrefix, + (oldKeyspace == null) ? "" : oldKeyspace.asInternal(), + newKeyspace.asInternal(), + DefaultDriverOption.REQUEST_WARN_IF_SET_KEYSPACE.getPath()); + } + this.keyspace = newKeyspace; + CompletableFuture result = new CompletableFuture<>(); + RunOrSchedule.on(adminExecutor, () -> singleThreaded.setKeyspace(newKeyspace, result)); + return result; + } + + public Map getPools() { + return pools; + } + + public ConcurrentMap getRepreparePayloads() { + return repreparePayloads; + } + + @NonNull + @Override + public CompletionStage closeFuture() { + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage closeAsync() { + RunOrSchedule.on(adminExecutor, singleThreaded::close); + return singleThreaded.closeFuture; + } + + @NonNull + @Override + public CompletionStage forceCloseAsync() { + RunOrSchedule.on(adminExecutor, singleThreaded::forceClose); + return singleThreaded.closeFuture; + } + + private class SingleThreaded { + + private final InternalDriverContext context; + private final ChannelPoolFactory channelPoolFactory; + private final CompletableFuture initFuture = new CompletableFuture<>(); + private boolean initWasCalled; + private final CompletableFuture closeFuture = new CompletableFuture<>(); + private boolean closeWasCalled; + private boolean forceCloseWasCalled; + private final Object distanceListenerKey; + private final ReplayingEventFilter distanceEventFilter = + new ReplayingEventFilter<>(this::processDistanceEvent); + private final Object stateListenerKey; + private final ReplayingEventFilter stateEventFilter = + new ReplayingEventFilter<>(this::processStateEvent); + private final Object topologyListenerKey; + // The pools that we have opened but have not finished initializing yet + private final Map> pending = new HashMap<>(); + // If we receive events while a pool is initializing, the last one is stored here + private final Map pendingDistanceEvents = new WeakHashMap<>(); + private final Map pendingStateEvents = new WeakHashMap<>(); + + private SingleThreaded(InternalDriverContext context) { + this.context = context; + this.channelPoolFactory = context.getChannelPoolFactory(); + this.distanceListenerKey = + context + .getEventBus() + .register( + DistanceEvent.class, RunOrSchedule.on(adminExecutor, this::onDistanceEvent)); + this.stateListenerKey = + context + .getEventBus() + .register(NodeStateEvent.class, RunOrSchedule.on(adminExecutor, this::onStateEvent)); + this.topologyListenerKey = + context + .getEventBus() + .register( + TopologyEvent.class, RunOrSchedule.on(adminExecutor, this::onTopologyEvent)); + } + + private void init(CqlIdentifier keyspace) { + assert adminExecutor.inEventLoop(); + if (initWasCalled) { + return; + } + initWasCalled = true; + + LOG.debug("[{}] Starting initialization", logPrefix); + + PoolManager.this.keyspace = keyspace; + + // Make sure we don't miss any event while the pools are initializing + distanceEventFilter.start(); + stateEventFilter.start(); + + Collection nodes = context.getMetadataManager().getMetadata().getNodes().values(); + List> poolStages = new ArrayList<>(nodes.size()); + for (Node node : nodes) { + NodeDistance distance = node.getDistance(); + if (distance == NodeDistance.IGNORED) { + LOG.debug("[{}] Skipping {} because it is IGNORED", logPrefix, node); + } else if (node.getState() == NodeState.FORCED_DOWN) { + LOG.debug("[{}] Skipping {} because it is FORCED_DOWN", logPrefix, node); + } else { + LOG.debug("[{}] Creating a pool for {}", logPrefix, node); + poolStages.add(channelPoolFactory.init(node, keyspace, distance, context, logPrefix)); + } + } + CompletableFutures.whenAllDone(poolStages, () -> this.onPoolsInit(poolStages), adminExecutor); + } + + private void onPoolsInit(List> poolStages) { + assert adminExecutor.inEventLoop(); + LOG.debug("[{}] All pools have finished initializing", logPrefix); + // We will only propagate an invalid keyspace error if all pools get it + boolean allInvalidKeyspaces = poolStages.size() > 0; + for (CompletionStage poolStage : poolStages) { + // Note: pool init always succeeds + ChannelPool pool = CompletableFutures.getCompleted(poolStage.toCompletableFuture()); + boolean invalidKeyspace = pool.isInvalidKeyspace(); + if (invalidKeyspace) { + LOG.debug("[{}] Pool to {} reports an invalid keyspace", logPrefix, pool.getNode()); + } + allInvalidKeyspaces &= invalidKeyspace; + pools.put(pool.getNode(), pool); + } + if (allInvalidKeyspaces) { + initFuture.completeExceptionally( + new InvalidKeyspaceException("Invalid keyspace " + keyspace.asCql(true))); + forceClose(); + } else { + LOG.debug("[{}] Initialization complete, ready", logPrefix); + initFuture.complete(null); + distanceEventFilter.markReady(); + stateEventFilter.markReady(); + } + } + + private void onDistanceEvent(DistanceEvent event) { + assert adminExecutor.inEventLoop(); + distanceEventFilter.accept(event); + } + + private void onStateEvent(NodeStateEvent event) { + assert adminExecutor.inEventLoop(); + stateEventFilter.accept(event); + } + + private void processDistanceEvent(DistanceEvent event) { + assert adminExecutor.inEventLoop(); + // no need to check closeWasCalled, because we stop listening for events one closed + DefaultNode node = event.node; + NodeDistance newDistance = event.distance; + if (pending.containsKey(node)) { + pendingDistanceEvents.put(node, event); + } else if (newDistance == NodeDistance.IGNORED) { + ChannelPool pool = pools.remove(node); + if (pool != null) { + LOG.debug("[{}] {} became IGNORED, destroying pool", logPrefix, node); + pool.closeAsync() + .exceptionally( + error -> { + Loggers.warnWithException(LOG, "[{}] Error closing pool", logPrefix, error); + return null; + }); + } + } else { + NodeState state = node.getState(); + if (state == NodeState.FORCED_DOWN) { + LOG.warn( + "[{}] {} became {} but it is FORCED_DOWN, ignoring", logPrefix, node, newDistance); + return; + } + ChannelPool pool = pools.get(node); + if (pool == null) { + LOG.debug( + "[{}] {} became {} and no pool found, initializing it", logPrefix, node, newDistance); + CompletionStage poolFuture = + channelPoolFactory.init(node, keyspace, newDistance, context, logPrefix); + pending.put(node, poolFuture); + poolFuture + .thenAcceptAsync(this::onPoolInitialized, adminExecutor) + .exceptionally(UncaughtExceptions::log); + } else { + LOG.debug("[{}] {} became {}, resizing it", logPrefix, node, newDistance); + pool.resize(newDistance); + } + } + } + + private void processStateEvent(NodeStateEvent event) { + assert adminExecutor.inEventLoop(); + // no need to check closeWasCalled, because we stop listening for events once closed + DefaultNode node = event.node; + NodeState oldState = event.oldState; + NodeState newState = event.newState; + if (pending.containsKey(node)) { + pendingStateEvents.put(node, event); + } else if (newState == NodeState.FORCED_DOWN) { + ChannelPool pool = pools.remove(node); + if (pool != null) { + LOG.debug("[{}] {} was FORCED_DOWN, destroying pool", logPrefix, node); + pool.closeAsync() + .exceptionally( + error -> { + Loggers.warnWithException(LOG, "[{}] Error closing pool", logPrefix, error); + return null; + }); + } + } else if (oldState == NodeState.FORCED_DOWN + && newState == NodeState.UP + && node.getDistance() != NodeDistance.IGNORED) { + LOG.debug("[{}] {} was forced back UP, initializing pool", logPrefix, node); + createOrReconnectPool(node); + } + } + + private void onTopologyEvent(TopologyEvent event) { + assert adminExecutor.inEventLoop(); + if (event.type == TopologyEvent.Type.SUGGEST_UP) { + context + .getMetadataManager() + .getMetadata() + .findNode(event.broadcastRpcAddress) + .ifPresent( + node -> { + if (node.getDistance() != NodeDistance.IGNORED) { + LOG.debug( + "[{}] Received a SUGGEST_UP event for {}, reconnecting pool now", + logPrefix, + node); + ChannelPool pool = pools.get(node); + if (pool != null) { + pool.reconnectNow(); + } + } + }); + } + } + + private void createOrReconnectPool(Node node) { + ChannelPool pool = pools.get(node); + if (pool == null) { + CompletionStage poolFuture = + channelPoolFactory.init(node, keyspace, node.getDistance(), context, logPrefix); + pending.put(node, poolFuture); + poolFuture + .thenAcceptAsync(this::onPoolInitialized, adminExecutor) + .exceptionally(UncaughtExceptions::log); + } else { + pool.reconnectNow(); + } + } + + private void onPoolInitialized(ChannelPool pool) { + assert adminExecutor.inEventLoop(); + Node node = pool.getNode(); + if (closeWasCalled) { + LOG.debug( + "[{}] Session closed while a pool to {} was initializing, closing it", logPrefix, node); + pool.forceCloseAsync(); + } else { + LOG.debug("[{}] New pool to {} initialized", logPrefix, node); + if (Objects.equals(keyspace, pool.getInitialKeyspaceName())) { + reprepareStatements(pool); + } else { + // The keyspace changed while the pool was being initialized, switch it now. + pool.setKeyspace(keyspace) + .handleAsync( + (result, error) -> { + if (error != null) { + Loggers.warnWithException( + LOG, "Error while switching keyspace to " + keyspace, error); + } + reprepareStatements(pool); + return null; + }, + adminExecutor); + } + } + } + + private void reprepareStatements(ChannelPool pool) { + assert adminExecutor.inEventLoop(); + if (config.getBoolean(DefaultDriverOption.REPREPARE_ENABLED)) { + new ReprepareOnUp( + logPrefix + "|" + pool.getNode().getEndPoint(), + pool, + repreparePayloads, + context, + () -> RunOrSchedule.on(adminExecutor, () -> onPoolReady(pool))) + .start(); + } else { + LOG.debug("[{}] Reprepare on up is disabled, skipping", logPrefix); + onPoolReady(pool); + } + } + + private void onPoolReady(ChannelPool pool) { + assert adminExecutor.inEventLoop(); + Node node = pool.getNode(); + pending.remove(node); + pools.put(node, pool); + DistanceEvent distanceEvent = pendingDistanceEvents.remove(node); + NodeStateEvent stateEvent = pendingStateEvents.remove(node); + if (stateEvent != null && stateEvent.newState == NodeState.FORCED_DOWN) { + LOG.debug( + "[{}] Received {} while the pool was initializing, processing it now", + logPrefix, + stateEvent); + processStateEvent(stateEvent); + } else if (distanceEvent != null) { + LOG.debug( + "[{}] Received {} while the pool was initializing, processing it now", + logPrefix, + distanceEvent); + processDistanceEvent(distanceEvent); + } + } + + private void setKeyspace(CqlIdentifier newKeyspace, CompletableFuture doneFuture) { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + doneFuture.complete(null); + return; + } + LOG.debug("[{}] Switching to keyspace {}", logPrefix, newKeyspace); + List> poolReadyFutures = Lists.newArrayListWithCapacity(pools.size()); + for (ChannelPool pool : pools.values()) { + poolReadyFutures.add(pool.setKeyspace(newKeyspace)); + } + CompletableFutures.completeFrom(CompletableFutures.allDone(poolReadyFutures), doneFuture); + } + + private void close() { + assert adminExecutor.inEventLoop(); + if (closeWasCalled) { + return; + } + closeWasCalled = true; + LOG.debug("[{}] Starting shutdown", logPrefix); + + // Stop listening for events + context.getEventBus().unregister(distanceListenerKey, DistanceEvent.class); + context.getEventBus().unregister(stateListenerKey, NodeStateEvent.class); + context.getEventBus().unregister(topologyListenerKey, TopologyEvent.class); + + List> closePoolStages = new ArrayList<>(pools.size()); + for (ChannelPool pool : pools.values()) { + closePoolStages.add(pool.closeAsync()); + } + CompletableFutures.whenAllDone( + closePoolStages, () -> onAllPoolsClosed(closePoolStages), adminExecutor); + } + + private void forceClose() { + assert adminExecutor.inEventLoop(); + if (forceCloseWasCalled) { + return; + } + forceCloseWasCalled = true; + LOG.debug( + "[{}] Starting forced shutdown (was {}closed before)", + logPrefix, + (closeWasCalled ? "" : "not ")); + + if (closeWasCalled) { + for (ChannelPool pool : pools.values()) { + pool.forceCloseAsync(); + } + } else { + List> closePoolStages = new ArrayList<>(pools.size()); + for (ChannelPool pool : pools.values()) { + closePoolStages.add(pool.forceCloseAsync()); + } + CompletableFutures.whenAllDone( + closePoolStages, () -> onAllPoolsClosed(closePoolStages), adminExecutor); + } + } + + private void onAllPoolsClosed(List> closePoolStages) { + assert adminExecutor.inEventLoop(); + Throwable firstError = null; + for (CompletionStage closePoolStage : closePoolStages) { + CompletableFuture closePoolFuture = closePoolStage.toCompletableFuture(); + assert closePoolFuture.isDone(); + if (closePoolFuture.isCompletedExceptionally()) { + Throwable error = CompletableFutures.getFailed(closePoolFuture); + if (firstError == null) { + firstError = error; + } else { + firstError.addSuppressed(error); + } + } + } + if (firstError != null) { + closeFuture.completeExceptionally(firstError); + } else { + LOG.debug("[{}] Shutdown complete", logPrefix); + closeFuture.complete(null); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/ReprepareOnUp.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/ReprepareOnUp.java new file mode 100644 index 00000000000..bd65c045673 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/ReprepareOnUp.java @@ -0,0 +1,252 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.adminrequest.ThrottledAdminRequestHandler; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.cql.CqlRequestHandler; +import com.datastax.oss.driver.internal.core.metrics.SessionMetricUpdater; +import com.datastax.oss.driver.internal.core.pool.ChannelPool; +import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.request.Prepare; +import com.datastax.oss.protocol.internal.request.Query; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Ensures that a newly added or restarted node knows all the prepared statements created from this + * driver instance. + * + *

See the comments in {@code reference.conf} for more explanations about this process. If any + * prepare request fail, we ignore the error because it will be retried on the fly (see {@link + * CqlRequestHandler}). + * + *

Logically this code belongs to {@link DefaultSession}, but it was extracted for modularity and + * testability. + */ +@ThreadSafe +class ReprepareOnUp { + + private static final Logger LOG = LoggerFactory.getLogger(ReprepareOnUp.class); + private static final Query QUERY_SERVER_IDS = + new Query("SELECT prepared_id FROM system.prepared_statements"); + + private final String logPrefix; + private final DriverChannel channel; + private final Map repreparePayloads; + private final Runnable whenPrepared; + private final boolean checkSystemTable; + private final int maxStatements; + private final int maxParallelism; + private final Duration timeout; + private final RequestThrottler throttler; + private final SessionMetricUpdater metricUpdater; + + // After the constructor, everything happens on the channel's event loop, so these fields do not + // need any synchronization. + private Set serverKnownIds; + private Queue toReprepare; + private int runningWorkers; + + ReprepareOnUp( + String logPrefix, + ChannelPool pool, + Map repreparePayloads, + InternalDriverContext context, + Runnable whenPrepared) { + + this.logPrefix = logPrefix; + this.channel = pool.next(); + this.repreparePayloads = repreparePayloads; + this.whenPrepared = whenPrepared; + this.throttler = context.getRequestThrottler(); + + DriverConfig config = context.getConfig(); + this.checkSystemTable = + config.getDefaultProfile().getBoolean(DefaultDriverOption.REPREPARE_CHECK_SYSTEM_TABLE); + this.timeout = config.getDefaultProfile().getDuration(DefaultDriverOption.REPREPARE_TIMEOUT); + this.maxStatements = + config.getDefaultProfile().getInt(DefaultDriverOption.REPREPARE_MAX_STATEMENTS); + this.maxParallelism = + config.getDefaultProfile().getInt(DefaultDriverOption.REPREPARE_MAX_PARALLELISM); + + this.metricUpdater = context.getMetricsFactory().getSessionUpdater(); + } + + void start() { + if (repreparePayloads.isEmpty()) { + LOG.debug("[{}] No statements to reprepare, done", logPrefix); + whenPrepared.run(); + } else if (this.channel == null) { + // Should not happen, but handle cleanly + LOG.debug("[{}] No channel available to reprepare, done", logPrefix); + whenPrepared.run(); + } else { + // Check log level because ConcurrentMap.size is not a constant operation + if (LOG.isDebugEnabled()) { + LOG.debug( + "[{}] {} statements to reprepare on newly added/up node", + logPrefix, + repreparePayloads.size()); + } + if (checkSystemTable) { + LOG.debug("[{}] Checking which statements the server knows about", logPrefix); + queryAsync(QUERY_SERVER_IDS, Collections.emptyMap(), "QUERY system.prepared_statements") + .whenComplete(this::gatherServerIds); + } else { + LOG.debug( + "[{}] {} is disabled, repreparing directly", + logPrefix, + DefaultDriverOption.REPREPARE_CHECK_SYSTEM_TABLE.getPath()); + RunOrSchedule.on( + channel.eventLoop(), + () -> { + serverKnownIds = Collections.emptySet(); + gatherPayloadsToReprepare(); + }); + } + } + } + + private void gatherServerIds(AdminResult rows, Throwable error) { + assert channel.eventLoop().inEventLoop(); + if (serverKnownIds == null) { + serverKnownIds = new HashSet<>(); + } + if (error != null) { + LOG.debug( + "[{}] Error querying system.prepared_statements ({}), proceeding without server ids", + logPrefix, + error.toString()); + gatherPayloadsToReprepare(); + } else { + for (AdminRow row : rows) { + serverKnownIds.add(row.getByteBuffer("prepared_id")); + } + if (rows.hasNextPage()) { + LOG.debug("[{}] system.prepared_statements has more pages", logPrefix); + rows.nextPage().whenComplete(this::gatherServerIds); + } else { + LOG.debug("[{}] Gathered {} server ids, proceeding", logPrefix, serverKnownIds.size()); + gatherPayloadsToReprepare(); + } + } + } + + private void gatherPayloadsToReprepare() { + assert channel.eventLoop().inEventLoop(); + toReprepare = new ArrayDeque<>(); + for (RepreparePayload payload : repreparePayloads.values()) { + if (serverKnownIds.contains(payload.id)) { + LOG.trace( + "[{}] Skipping statement {} because it is already known to the server", + logPrefix, + Bytes.toHexString(payload.id)); + } else { + if (maxStatements > 0 && toReprepare.size() == maxStatements) { + LOG.debug( + "[{}] Limiting number of statements to reprepare to {} as configured, " + + "but there are more", + logPrefix, + maxStatements); + break; + } else { + toReprepare.add(payload); + } + } + } + if (toReprepare.isEmpty()) { + LOG.debug( + "[{}] No statements to reprepare that are not known by the server already, done", + logPrefix); + whenPrepared.run(); + } else { + startWorkers(); + } + } + + private void startWorkers() { + assert channel.eventLoop().inEventLoop(); + runningWorkers = Math.min(maxParallelism, toReprepare.size()); + LOG.debug( + "[{}] Repreparing {} statements with {} parallel workers", + logPrefix, + toReprepare.size(), + runningWorkers); + for (int i = 0; i < runningWorkers; i++) { + startWorker(); + } + } + + private void startWorker() { + assert channel.eventLoop().inEventLoop(); + if (toReprepare.isEmpty()) { + runningWorkers -= 1; + if (runningWorkers == 0) { + LOG.debug("[{}] All workers finished, done", logPrefix); + whenPrepared.run(); + } + } else { + RepreparePayload payload = toReprepare.poll(); + queryAsync( + new Prepare( + payload.query, (payload.keyspace == null ? null : payload.keyspace.asInternal())), + payload.customPayload, + String.format("Reprepare '%s'", payload.query)) + .handle( + (result, error) -> { + // Don't log, AdminRequestHandler does already + startWorker(); + return null; + }); + } + } + + @VisibleForTesting + protected CompletionStage queryAsync( + Message message, Map customPayload, String debugString) { + ThrottledAdminRequestHandler reprepareHandler = + new ThrottledAdminRequestHandler( + channel, + message, + customPayload, + timeout, + throttler, + metricUpdater, + logPrefix, + debugString); + return reprepareHandler.start(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/RepreparePayload.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/RepreparePayload.java new file mode 100644 index 00000000000..eaa9541a59f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/RepreparePayload.java @@ -0,0 +1,48 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.core.cql.DefaultPreparedStatement; +import java.nio.ByteBuffer; +import java.util.Map; +import net.jcip.annotations.Immutable; + +/** + * The information that's necessary to reprepare an already prepared statement, in case we hit a + * node that doesn't have it in its cache. + * + *

Make sure the object that's returned to the client (e.g. {@link DefaultPreparedStatement} for + * CQL statements) keeps a reference to this. + */ +@Immutable +public class RepreparePayload { + public final ByteBuffer id; + public final String query; + + /** The keyspace that is set independently from the query string (see CASSANDRA-10145) */ + public final CqlIdentifier keyspace; + + public final Map customPayload; + + public RepreparePayload( + ByteBuffer id, String query, CqlIdentifier keyspace, Map customPayload) { + this.id = id; + this.query = query; + this.keyspace = keyspace; + this.customPayload = customPayload; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/RequestProcessor.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/RequestProcessor.java new file mode 100644 index 00000000000..b61ccebb090 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/RequestProcessor.java @@ -0,0 +1,52 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import com.datastax.oss.driver.api.core.cql.PrepareRequest; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; + +/** + * Handles a type of request in the driver. + * + *

By default, the driver supports CQL {@link Statement queries} and {@link PrepareRequest + * preparation requests}. New processors can be plugged in to handle new types of requests. + * + * @param the type of request accepted. + * @param the type of result when a request is processed. + */ +public interface RequestProcessor { + + /** + * Whether the processor can produce the given result from the given request. + * + *

Processors will be tried in the order they were registered. The first processor for which + * this method returns true will be used. + */ + boolean canProcess(Request request, GenericType resultType); + + /** Processes the given request, producing a result. */ + ResultT process( + RequestT request, + DefaultSession session, + InternalDriverContext context, + String sessionLogPrefix); + + /** Builds a failed result to directly report the given error. */ + ResultT newFailure(RuntimeException error); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/RequestProcessorRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/RequestProcessorRegistry.java new file mode 100644 index 00000000000..aca57fda97f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/RequestProcessorRegistry.java @@ -0,0 +1,81 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.cql.CqlPrepareAsyncProcessor; +import com.datastax.oss.driver.internal.core.cql.CqlPrepareSyncProcessor; +import com.datastax.oss.driver.internal.core.cql.CqlRequestAsyncProcessor; +import com.datastax.oss.driver.internal.core.cql.CqlRequestSyncProcessor; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ThreadSafe +public class RequestProcessorRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(RequestProcessorRegistry.class); + + public static RequestProcessorRegistry defaultCqlProcessors(String logPrefix) { + CqlRequestAsyncProcessor requestAsyncProcessor = new CqlRequestAsyncProcessor(); + CqlRequestSyncProcessor requestSyncProcessor = + new CqlRequestSyncProcessor(requestAsyncProcessor); + CqlPrepareAsyncProcessor prepareAsyncProcessor = new CqlPrepareAsyncProcessor(); + CqlPrepareSyncProcessor prepareSyncProcessor = + new CqlPrepareSyncProcessor(prepareAsyncProcessor); + + return new RequestProcessorRegistry( + logPrefix, + requestAsyncProcessor, + requestSyncProcessor, + prepareAsyncProcessor, + prepareSyncProcessor); + } + + private final String logPrefix; + // Effectively immutable: the contents are never modified after construction + private final RequestProcessor[] processors; + + public RequestProcessorRegistry(String logPrefix, RequestProcessor... processors) { + this.logPrefix = logPrefix; + this.processors = processors; + } + + public RequestProcessor processorFor( + RequestT request, GenericType resultType) { + + for (RequestProcessor processor : processors) { + if (processor.canProcess(request, resultType)) { + LOG.trace("[{}] Using {} to process {}", logPrefix, processor, request); + // The cast is safe provided that the processor implements canProcess correctly + @SuppressWarnings("unchecked") + RequestProcessor result = + (RequestProcessor) processor; + return result; + } else { + LOG.trace("[{}] {} cannot process {}, trying next", logPrefix, processor, request); + } + } + throw new IllegalArgumentException("No request processor found for " + request); + } + + /** This creates a defensive copy on every call, do not overuse. */ + public Iterable> getProcessors() { + return ImmutableList.copyOf(processors); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/SchemaListenerNotifier.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/SchemaListenerNotifier.java new file mode 100644 index 00000000000..4e4c9765157 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/SchemaListenerNotifier.java @@ -0,0 +1,145 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.metadata.schema.events.AggregateChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.FunctionChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.KeyspaceChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.TableChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.TypeChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.ViewChangeEvent; +import com.datastax.oss.driver.internal.core.util.concurrent.RunOrSchedule; +import io.netty.util.concurrent.EventExecutor; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +class SchemaListenerNotifier { + + private final SchemaChangeListener listener; + private final EventExecutor adminExecutor; + + SchemaListenerNotifier( + SchemaChangeListener listener, EventBus eventBus, EventExecutor adminExecutor) { + this.listener = listener; + this.adminExecutor = adminExecutor; + + // No need to unregister at shutdown, this component has the same lifecycle as the cluster + eventBus.register( + AggregateChangeEvent.class, RunOrSchedule.on(adminExecutor, this::onAggregateChangeEvent)); + eventBus.register( + FunctionChangeEvent.class, RunOrSchedule.on(adminExecutor, this::onFunctionChangeEvent)); + eventBus.register( + KeyspaceChangeEvent.class, RunOrSchedule.on(adminExecutor, this::onKeyspaceChangeEvent)); + eventBus.register( + TableChangeEvent.class, RunOrSchedule.on(adminExecutor, this::onTableChangeEvent)); + eventBus.register( + TypeChangeEvent.class, RunOrSchedule.on(adminExecutor, this::onTypeChangeEvent)); + eventBus.register( + ViewChangeEvent.class, RunOrSchedule.on(adminExecutor, this::onViewChangeEvent)); + } + + private void onAggregateChangeEvent(AggregateChangeEvent event) { + assert adminExecutor.inEventLoop(); + switch (event.changeType) { + case CREATED: + listener.onAggregateCreated(event.newAggregate); + break; + case UPDATED: + listener.onAggregateUpdated(event.newAggregate, event.oldAggregate); + break; + case DROPPED: + listener.onAggregateDropped(event.oldAggregate); + break; + } + } + + private void onFunctionChangeEvent(FunctionChangeEvent event) { + assert adminExecutor.inEventLoop(); + switch (event.changeType) { + case CREATED: + listener.onFunctionCreated(event.newFunction); + break; + case UPDATED: + listener.onFunctionUpdated(event.newFunction, event.oldFunction); + break; + case DROPPED: + listener.onFunctionDropped(event.oldFunction); + break; + } + } + + private void onKeyspaceChangeEvent(KeyspaceChangeEvent event) { + assert adminExecutor.inEventLoop(); + switch (event.changeType) { + case CREATED: + listener.onKeyspaceCreated(event.newKeyspace); + break; + case UPDATED: + listener.onKeyspaceUpdated(event.newKeyspace, event.oldKeyspace); + break; + case DROPPED: + listener.onKeyspaceDropped(event.oldKeyspace); + break; + } + } + + private void onTableChangeEvent(TableChangeEvent event) { + assert adminExecutor.inEventLoop(); + switch (event.changeType) { + case CREATED: + listener.onTableCreated(event.newTable); + break; + case UPDATED: + listener.onTableUpdated(event.newTable, event.oldTable); + break; + case DROPPED: + listener.onTableDropped(event.oldTable); + break; + } + } + + private void onTypeChangeEvent(TypeChangeEvent event) { + assert adminExecutor.inEventLoop(); + switch (event.changeType) { + case CREATED: + listener.onUserDefinedTypeCreated(event.newType); + break; + case UPDATED: + listener.onUserDefinedTypeUpdated(event.newType, event.oldType); + break; + case DROPPED: + listener.onUserDefinedTypeDropped(event.oldType); + break; + } + } + + private void onViewChangeEvent(ViewChangeEvent event) { + assert adminExecutor.inEventLoop(); + switch (event.changeType) { + case CREATED: + listener.onViewCreated(event.newView); + break; + case UPDATED: + listener.onViewUpdated(event.newView, event.oldView); + break; + case DROPPED: + listener.onViewDropped(event.oldView); + break; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/SessionWrapper.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/SessionWrapper.java new file mode 100644 index 00000000000..c697718a2d0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/SessionWrapper.java @@ -0,0 +1,135 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metrics.Metrics; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Optional; +import java.util.concurrent.CompletionStage; +import net.jcip.annotations.ThreadSafe; + +/** + * Utility class to wrap a session. + * + *

This will typically be used to mix in a convenience interface from a 3rd-party extension: + * + *

{@code
+ * class ReactiveSessionWrapper extends SessionWrapper implements ReactiveSession {
+ *   public ReactiveSessionWrapper(Session delegate) {
+ *     super(delegate);
+ *   }
+ * }
+ * }
+ */ +@ThreadSafe +public class SessionWrapper implements Session { + + private final Session delegate; + + public SessionWrapper(@NonNull Session delegate) { + this.delegate = delegate; + } + + @NonNull + public Session getDelegate() { + return delegate; + } + + @NonNull + @Override + public String getName() { + return delegate.getName(); + } + + @NonNull + @Override + public Metadata getMetadata() { + return delegate.getMetadata(); + } + + @Override + public boolean isSchemaMetadataEnabled() { + return delegate.isSchemaMetadataEnabled(); + } + + @NonNull + @Override + public CompletionStage setSchemaMetadataEnabled(@Nullable Boolean newValue) { + return delegate.setSchemaMetadataEnabled(newValue); + } + + @NonNull + @Override + public CompletionStage refreshSchemaAsync() { + return delegate.refreshSchemaAsync(); + } + + @NonNull + @Override + public CompletionStage checkSchemaAgreementAsync() { + return delegate.checkSchemaAgreementAsync(); + } + + @NonNull + @Override + public DriverContext getContext() { + return delegate.getContext(); + } + + @NonNull + @Override + public Optional getKeyspace() { + return delegate.getKeyspace(); + } + + @NonNull + @Override + public Optional getMetrics() { + return delegate.getMetrics(); + } + + @Nullable + @Override + public ResultT execute( + @NonNull RequestT request, @NonNull GenericType resultType) { + return delegate.execute(request, resultType); + } + + @NonNull + @Override + public CompletionStage closeFuture() { + return delegate.closeFuture(); + } + + @NonNull + @Override + public CompletionStage closeAsync() { + return delegate.closeAsync(); + } + + @NonNull + @Override + public CompletionStage forceCloseAsync() { + return delegate.forceCloseAsync(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/ConcurrencyLimitingRequestThrottler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/ConcurrencyLimitingRequestThrottler.java new file mode 100644 index 00000000000..ebfc838f4ac --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/ConcurrencyLimitingRequestThrottler.java @@ -0,0 +1,206 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session.throttling; + +import com.datastax.oss.driver.api.core.RequestThrottlingException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.api.core.session.throttling.Throttled; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.concurrent.locks.ReentrantLock; +import net.jcip.annotations.GuardedBy; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A request throttler that limits the number of concurrent requests. + * + *

To activate this throttler, modify the {@code advanced.throttler} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.throttler {
+ *     class = ConcurrencyLimitingRequestThrottler
+ *     max-concurrent-requests = 10000
+ *     max-queue-size = 10000
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class ConcurrencyLimitingRequestThrottler implements RequestThrottler { + + private static final Logger LOG = + LoggerFactory.getLogger(ConcurrencyLimitingRequestThrottler.class); + + private final String logPrefix; + private final int maxConcurrentRequests; + private final int maxQueueSize; + + private final ReentrantLock lock = new ReentrantLock(); + + @GuardedBy("lock") + private int concurrentRequests; + + @GuardedBy("lock") + private Deque queue = new ArrayDeque<>(); + + @GuardedBy("lock") + private boolean closed; + + public ConcurrencyLimitingRequestThrottler(DriverContext context) { + this.logPrefix = context.getSessionName(); + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + this.maxConcurrentRequests = + config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_CONCURRENT_REQUESTS); + this.maxQueueSize = config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE); + LOG.debug( + "[{}] Initializing with maxConcurrentRequests = {}, maxQueueSize = {}", + logPrefix, + maxConcurrentRequests, + maxQueueSize); + } + + @Override + public void register(@NonNull Throttled request) { + lock.lock(); + try { + if (closed) { + LOG.trace("[{}] Rejecting request after shutdown", logPrefix); + fail(request, "The session is shutting down"); + } else if (queue.isEmpty() && concurrentRequests < maxConcurrentRequests) { + // We have capacity for one more concurrent request + LOG.trace("[{}] Starting newly registered request", logPrefix); + concurrentRequests += 1; + request.onThrottleReady(false); + } else if (queue.size() < maxQueueSize) { + LOG.trace("[{}] Enqueuing request", logPrefix); + queue.add(request); + } else { + LOG.trace("[{}] Rejecting request because of full queue", logPrefix); + fail( + request, + String.format( + "The session has reached its maximum capacity " + + "(concurrent requests: %d, queue size: %d)", + maxConcurrentRequests, maxQueueSize)); + } + } finally { + lock.unlock(); + } + } + + @Override + public void signalSuccess(@NonNull Throttled request) { + lock.lock(); + try { + onRequestDone(); + } finally { + lock.unlock(); + } + } + + @Override + public void signalError(@NonNull Throttled request, @NonNull Throwable error) { + signalSuccess(request); // not treated differently + } + + @Override + public void signalTimeout(@NonNull Throttled request) { + lock.lock(); + try { + if (!closed) { + if (queue.remove(request)) { // The request timed out before it was active + LOG.trace("[{}] Removing timed out request from the queue", logPrefix); + } else { + onRequestDone(); + } + } + } finally { + lock.unlock(); + } + } + + @SuppressWarnings("GuardedBy") // this method is only called with the lock held + private void onRequestDone() { + assert lock.isHeldByCurrentThread(); + if (!closed) { + if (queue.isEmpty()) { + concurrentRequests -= 1; + } else { + LOG.trace("[{}] Starting dequeued request", logPrefix); + queue.poll().onThrottleReady(true); + // don't touch concurrentRequests since we finished one but started another + } + } + } + + @Override + public void close() { + lock.lock(); + try { + closed = true; + LOG.debug("[{}] Rejecting {} queued requests after shutdown", logPrefix, queue.size()); + for (Throttled request : queue) { + fail(request, "The session is shutting down"); + } + } finally { + lock.unlock(); + } + } + + public int getQueueSize() { + lock.lock(); + try { + return queue.size(); + } finally { + lock.unlock(); + } + } + + @VisibleForTesting + int getConcurrentRequests() { + lock.lock(); + try { + return concurrentRequests; + } finally { + lock.unlock(); + } + } + + @VisibleForTesting + Deque getQueue() { + lock.lock(); + try { + return queue; + } finally { + lock.unlock(); + } + } + + private static void fail(Throttled request, String message) { + request.onThrottleFailure(new RequestThrottlingException(message)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/NanoClock.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/NanoClock.java new file mode 100644 index 00000000000..65587e7c3a0 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/NanoClock.java @@ -0,0 +1,21 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session.throttling; + +/** A thin wrapper around {@link System#nanoTime()}, to simplify testing. */ +interface NanoClock { + long nanoTime(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/PassThroughRequestThrottler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/PassThroughRequestThrottler.java new file mode 100644 index 00000000000..11fa2632a34 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/PassThroughRequestThrottler.java @@ -0,0 +1,74 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session.throttling; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.api.core.session.throttling.Throttled; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import net.jcip.annotations.ThreadSafe; + +/** + * A request throttler that does not enforce any kind of limitation: requests are always executed + * immediately. + * + *

To activate this throttler, modify the {@code advanced.throttler} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.throttler {
+ *     class = PassThroughRequestThrottler
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class PassThroughRequestThrottler implements RequestThrottler { + + @SuppressWarnings("unused") + public PassThroughRequestThrottler(DriverContext context) { + // nothing to do + } + + @Override + public void register(@NonNull Throttled request) { + request.onThrottleReady(false); + } + + @Override + public void signalSuccess(@NonNull Throttled request) { + // nothing to do + } + + @Override + public void signalError(@NonNull Throttled request, @NonNull Throwable error) { + // nothing to do + } + + @Override + public void signalTimeout(@NonNull Throttled request) { + // nothing to do + } + + @Override + public void close() throws IOException { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/RateLimitingRequestThrottler.java b/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/RateLimitingRequestThrottler.java new file mode 100644 index 00000000000..ab4035a4d46 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/session/throttling/RateLimitingRequestThrottler.java @@ -0,0 +1,270 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session.throttling; + +import com.datastax.oss.driver.api.core.RequestThrottlingException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.session.throttling.RequestThrottler; +import com.datastax.oss.driver.api.core.session.throttling.Throttled; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.util.concurrent.EventExecutor; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import net.jcip.annotations.GuardedBy; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A request throttler that limits the rate of requests per second. + * + *

To activate this throttler, modify the {@code advanced.throttler} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.throttler {
+ *     class = RateLimitingRequestThrottler
+ *     max-requests-per-second = 10000
+ *     max-queue-size = 10000
+ *     drain-interval = 10 milliseconds
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class RateLimitingRequestThrottler implements RequestThrottler { + + private static final Logger LOG = LoggerFactory.getLogger(RateLimitingRequestThrottler.class); + + private final String logPrefix; + private final NanoClock clock; + private final int maxRequestsPerSecond; + private final int maxQueueSize; + private final long drainIntervalNanos; + private final EventExecutor scheduler; + + private final ReentrantLock lock = new ReentrantLock(); + + @GuardedBy("lock") + private long lastUpdateNanos; + + @GuardedBy("lock") + private int storedPermits; + + @GuardedBy("lock") + private final Deque queue = new ArrayDeque<>(); + + @GuardedBy("lock") + private boolean closed; + + @SuppressWarnings("unused") + public RateLimitingRequestThrottler(DriverContext context) { + this(context, System::nanoTime); + } + + @VisibleForTesting + RateLimitingRequestThrottler(DriverContext context, NanoClock clock) { + this.logPrefix = context.getSessionName(); + this.clock = clock; + + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + + this.maxRequestsPerSecond = + config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_REQUESTS_PER_SECOND); + this.maxQueueSize = config.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE); + Duration drainInterval = + config.getDuration(DefaultDriverOption.REQUEST_THROTTLER_DRAIN_INTERVAL); + this.drainIntervalNanos = drainInterval.toNanos(); + + this.lastUpdateNanos = clock.nanoTime(); + // Start with one second worth of permits to avoid delaying initial requests + this.storedPermits = maxRequestsPerSecond; + + this.scheduler = + ((InternalDriverContext) context).getNettyOptions().adminEventExecutorGroup().next(); + + LOG.debug( + "[{}] Initializing with maxRequestsPerSecond = {}, maxQueueSize = {}, drainInterval = {}", + logPrefix, + maxRequestsPerSecond, + maxQueueSize, + drainInterval); + } + + @Override + public void register(@NonNull Throttled request) { + long now = clock.nanoTime(); + lock.lock(); + try { + if (closed) { + LOG.trace("[{}] Rejecting request after shutdown", logPrefix); + fail(request, "The session is shutting down"); + } else if (queue.isEmpty() && acquire(now, 1) == 1) { + LOG.trace("[{}] Starting newly registered request", logPrefix); + request.onThrottleReady(false); + } else if (queue.size() < maxQueueSize) { + LOG.trace("[{}] Enqueuing request", logPrefix); + if (queue.isEmpty()) { + scheduler.schedule(this::drain, drainIntervalNanos, TimeUnit.NANOSECONDS); + } + queue.add(request); + } else { + LOG.trace("[{}] Rejecting request because of full queue", logPrefix); + fail( + request, + String.format( + "The session has reached its maximum capacity " + + "(requests/s: %d, queue size: %d)", + maxRequestsPerSecond, maxQueueSize)); + } + } finally { + lock.unlock(); + } + } + + // Runs periodically when the queue is not empty. It tries to dequeue as much as possible while + // staying under the target rate. If it does not completely drain the queue, it reschedules + // itself. + private void drain() { + assert scheduler.inEventLoop(); + long now = clock.nanoTime(); + lock.lock(); + try { + if (closed || queue.isEmpty()) { + return; + } + int toDequeue = acquire(now, queue.size()); + LOG.trace("[{}] Dequeuing {}/{} elements", logPrefix, toDequeue, queue.size()); + for (int i = 0; i < toDequeue; i++) { + LOG.trace("[{}] Starting dequeued request", logPrefix); + queue.poll().onThrottleReady(true); + } + if (!queue.isEmpty()) { + LOG.trace( + "[{}] {} elements remaining in queue, rescheduling drain task", + logPrefix, + queue.size()); + scheduler.schedule(this::drain, drainIntervalNanos, TimeUnit.NANOSECONDS); + } + } finally { + lock.unlock(); + } + } + + @Override + public void signalSuccess(@NonNull Throttled request) { + // nothing to do + } + + @Override + public void signalError(@NonNull Throttled request, @NonNull Throwable error) { + // nothing to do + } + + @Override + public void signalTimeout(@NonNull Throttled request) { + lock.lock(); + try { + if (!closed && queue.remove(request)) { // The request timed out before it was active + LOG.trace("[{}] Removing timed out request from the queue", logPrefix); + } + } finally { + lock.unlock(); + } + } + + @Override + public void close() { + lock.lock(); + try { + closed = true; + LOG.debug("[{}] Rejecting {} queued requests after shutdown", logPrefix, queue.size()); + for (Throttled request : queue) { + fail(request, "The session is shutting down"); + } + } finally { + lock.unlock(); + } + } + + @SuppressWarnings("GuardedBy") // this method is only called with the lock held + private int acquire(long currentTimeNanos, int wantedPermits) { + assert lock.isHeldByCurrentThread() && !closed; + + long elapsedNanos = currentTimeNanos - lastUpdateNanos; + + if (elapsedNanos >= 1_000_000_000) { + // created more than the max, so whatever was stored, the sum will be capped to the max + storedPermits = maxRequestsPerSecond; + lastUpdateNanos = currentTimeNanos; + } else if (elapsedNanos > 0) { + int createdPermits = (int) (elapsedNanos * maxRequestsPerSecond / 1_000_000_000); + if (createdPermits > 0) { + // Only reset interval if we've generated permits, otherwise we might continually reset + // before we get the chance to generate anything. + lastUpdateNanos = currentTimeNanos; + } + storedPermits = Math.min(storedPermits + createdPermits, maxRequestsPerSecond); + } + + int returned = (storedPermits >= wantedPermits) ? wantedPermits : storedPermits; + storedPermits = Math.max(storedPermits - wantedPermits, 0); + return returned; + } + + public int getQueueSize() { + lock.lock(); + try { + return queue.size(); + } finally { + lock.unlock(); + } + } + + @VisibleForTesting + int getStoredPermits() { + lock.lock(); + try { + return storedPermits; + } finally { + lock.unlock(); + } + } + + @VisibleForTesting + Deque getQueue() { + lock.lock(); + try { + return queue; + } finally { + lock.unlock(); + } + } + + private static void fail(Throttled request, String message) { + request.onThrottleFailure(new RequestThrottlingException(message)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/specex/ConstantSpeculativeExecutionPolicy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/specex/ConstantSpeculativeExecutionPolicy.java new file mode 100644 index 00000000000..a33b0d33cc9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/specex/ConstantSpeculativeExecutionPolicy.java @@ -0,0 +1,81 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.specex; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.ThreadSafe; + +/** + * A policy that schedules a configurable number of speculative executions, separated by a fixed + * delay. + * + *

To activate this policy, modify the {@code advanced.speculative-execution-policy} section in + * the driver configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.speculative-execution-policy {
+ *     class = ConstantSpeculativeExecutionPolicy
+ *     max-executions = 3
+ *     delay = 100 milliseconds
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class ConstantSpeculativeExecutionPolicy implements SpeculativeExecutionPolicy { + + private final int maxExecutions; + private final long constantDelayMillis; + + public ConstantSpeculativeExecutionPolicy(DriverContext context, String profileName) { + DriverExecutionProfile config = context.getConfig().getProfile(profileName); + this.maxExecutions = config.getInt(DefaultDriverOption.SPECULATIVE_EXECUTION_MAX); + if (this.maxExecutions < 1) { + throw new IllegalArgumentException("Max must be at least 1"); + } + this.constantDelayMillis = + config.getDuration(DefaultDriverOption.SPECULATIVE_EXECUTION_DELAY).toMillis(); + if (this.constantDelayMillis < 0) { + throw new IllegalArgumentException("Delay must be positive or 0"); + } + } + + @Override + public long nextExecution( + @NonNull @SuppressWarnings("unused") Node node, + @Nullable @SuppressWarnings("unused") CqlIdentifier keyspace, + @NonNull @SuppressWarnings("unused") Request request, + int runningExecutions) { + assert runningExecutions >= 1; + return (runningExecutions < maxExecutions) ? constantDelayMillis : -1; + } + + @Override + public void close() { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/specex/NoSpeculativeExecutionPolicy.java b/core/src/main/java/com/datastax/oss/driver/internal/core/specex/NoSpeculativeExecutionPolicy.java new file mode 100644 index 00000000000..b758e206c3a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/specex/NoSpeculativeExecutionPolicy.java @@ -0,0 +1,67 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.specex; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.ThreadSafe; + +/** + * A policy that never triggers speculative executions. + * + *

To activate this policy, modify the {@code advanced.speculative-execution-policy} section in + * the driver configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.speculative-execution-policy {
+ *     class = NoSpeculativeExecutionPolicy
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class NoSpeculativeExecutionPolicy implements SpeculativeExecutionPolicy { + + public NoSpeculativeExecutionPolicy( + @SuppressWarnings("unused") DriverContext context, + @SuppressWarnings("unused") String profileName) { + // nothing to do + } + + @Override + @SuppressWarnings("unused") + public long nextExecution( + @NonNull Node node, + @Nullable CqlIdentifier keyspace, + @NonNull Request request, + int runningExecutions) { + // never start speculative executions + return -1; + } + + @Override + public void close() { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/DefaultSslEngineFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/DefaultSslEngineFactory.java new file mode 100644 index 00000000000..e60088b7b25 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/DefaultSslEngineFactory.java @@ -0,0 +1,164 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.ssl; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.List; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManagerFactory; +import net.jcip.annotations.ThreadSafe; + +/** + * Default SSL implementation. + * + *

To activate this class, add an {@code advanced.ssl-engine-factory} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.ssl-engine-factory {
+ *     class = DefaultSslEngineFactory
+ *     cipher-suites = [ "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA" ]
+ *     hostname-validation = false
+ *     truststore-path = /path/to/client.truststore
+ *     truststore-password = password123
+ *     keystore-path = /path/to/client.keystore
+ *     keystore-password = password123
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class DefaultSslEngineFactory implements SslEngineFactory { + + private final SSLContext sslContext; + private final String[] cipherSuites; + private final boolean requireHostnameValidation; + + /** Builds a new instance from the driver configuration. */ + public DefaultSslEngineFactory(DriverContext driverContext) { + DriverExecutionProfile config = driverContext.getConfig().getDefaultProfile(); + try { + this.sslContext = buildContext(config); + } catch (Exception e) { + throw new IllegalStateException("Cannot initialize SSL Context", e); + } + if (config.isDefined(DefaultDriverOption.SSL_CIPHER_SUITES)) { + List list = config.getStringList(DefaultDriverOption.SSL_CIPHER_SUITES); + String tmp[] = new String[list.size()]; + this.cipherSuites = list.toArray(tmp); + } else { + this.cipherSuites = null; + } + this.requireHostnameValidation = + config.getBoolean(DefaultDriverOption.SSL_HOSTNAME_VALIDATION, true); + } + + @NonNull + @Override + public SSLEngine newSslEngine(@NonNull EndPoint remoteEndpoint) { + SSLEngine engine; + SocketAddress remoteAddress = remoteEndpoint.resolve(); + if (remoteAddress instanceof InetSocketAddress) { + InetSocketAddress socketAddress = (InetSocketAddress) remoteAddress; + engine = sslContext.createSSLEngine(socketAddress.getHostName(), socketAddress.getPort()); + } else { + engine = sslContext.createSSLEngine(); + } + engine.setUseClientMode(true); + if (cipherSuites != null) { + engine.setEnabledCipherSuites(cipherSuites); + } + if (requireHostnameValidation) { + SSLParameters parameters = engine.getSSLParameters(); + parameters.setEndpointIdentificationAlgorithm("HTTPS"); + engine.setSSLParameters(parameters); + } + return engine; + } + + protected SSLContext buildContext(DriverExecutionProfile config) throws Exception { + if (config.isDefined(DefaultDriverOption.SSL_KEYSTORE_PATH) + || config.isDefined(DefaultDriverOption.SSL_TRUSTSTORE_PATH)) { + SSLContext context = SSLContext.getInstance("SSL"); + + // initialize truststore if configured. + TrustManagerFactory tmf = null; + if (config.isDefined(DefaultDriverOption.SSL_TRUSTSTORE_PATH)) { + try (InputStream tsf = + Files.newInputStream( + Paths.get(config.getString(DefaultDriverOption.SSL_TRUSTSTORE_PATH)))) { + KeyStore ts = KeyStore.getInstance("JKS"); + char[] password = + config.isDefined(DefaultDriverOption.SSL_TRUSTSTORE_PASSWORD) + ? config.getString(DefaultDriverOption.SSL_TRUSTSTORE_PASSWORD).toCharArray() + : null; + ts.load(tsf, password); + tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts); + } + } + + // initialize keystore if configured. + KeyManagerFactory kmf = null; + if (config.isDefined(DefaultDriverOption.SSL_KEYSTORE_PATH)) { + try (InputStream ksf = + Files.newInputStream( + Paths.get(config.getString(DefaultDriverOption.SSL_KEYSTORE_PATH)))) { + KeyStore ks = KeyStore.getInstance("JKS"); + char[] password = + config.isDefined(DefaultDriverOption.SSL_KEYSTORE_PASSWORD) + ? config.getString(DefaultDriverOption.SSL_KEYSTORE_PASSWORD).toCharArray() + : null; + ks.load(ksf, password); + kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, password); + } + } + + context.init( + kmf != null ? kmf.getKeyManagers() : null, + tmf != null ? tmf.getTrustManagers() : null, + new SecureRandom()); + return context; + } else { + // if both keystore and truststore aren't configured, use default SSLContext. + return SSLContext.getDefault(); + } + } + + @Override + public void close() throws Exception { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/JdkSslHandlerFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/JdkSslHandlerFactory.java new file mode 100644 index 00000000000..73cb73660fb --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/JdkSslHandlerFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.ssl; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.ssl.SslEngineFactory; +import io.netty.channel.Channel; +import io.netty.handler.ssl.SslHandler; +import javax.net.ssl.SSLEngine; +import net.jcip.annotations.ThreadSafe; + +/** SSL handler factory used when JDK-based SSL was configured through the driver's public API. */ +@ThreadSafe +public class JdkSslHandlerFactory implements SslHandlerFactory { + private final SslEngineFactory sslEngineFactory; + + public JdkSslHandlerFactory(SslEngineFactory sslEngineFactory) { + this.sslEngineFactory = sslEngineFactory; + } + + @Override + public SslHandler newSslHandler(Channel channel, EndPoint remoteEndpoint) { + SSLEngine engine = sslEngineFactory.newSslEngine(remoteEndpoint); + return new SslHandler(engine); + } + + @Override + public void close() throws Exception { + sslEngineFactory.close(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/SslHandlerFactory.java b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/SslHandlerFactory.java new file mode 100644 index 00000000000..3a96b067ada --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/ssl/SslHandlerFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.ssl; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.internal.core.context.DefaultDriverContext; +import io.netty.channel.Channel; +import io.netty.handler.ssl.SslHandler; + +/** + * Low-level SSL extension point. + * + *

SSL is separated into two interfaces to avoid exposing Netty classes in our public API: + * + *

    + *
  • "normal" (JDK-based) SSL is part of the public API, and can be configured via an instance + * of {@link com.datastax.oss.driver.api.core.ssl.SslEngineFactory} defined in the driver + * configuration. + *
  • this interface deals with Netty handlers directly. It can be used for more advanced cases, + * like using Netty's native OpenSSL integration instead of the JDK. This is considered expert + * level, and therefore part of our internal API. + *
+ * + * @see DefaultDriverContext#buildSslHandlerFactory() + */ +public interface SslHandlerFactory extends AutoCloseable { + SslHandler newSslHandler(Channel channel, EndPoint remoteEndpoint); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/time/AtomicTimestampGenerator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/time/AtomicTimestampGenerator.java new file mode 100644 index 00000000000..28bd5fdf2e4 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/time/AtomicTimestampGenerator.java @@ -0,0 +1,70 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.time; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import java.util.concurrent.atomic.AtomicLong; +import net.jcip.annotations.ThreadSafe; + +/** + * A timestamp generator that guarantees monotonically increasing timestamps across all client + * threads, and logs warnings when timestamps drift in the future. + * + *

To activate this generator, modify the {@code advanced.timestamp-generator} section in the + * driver configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.timestamp-generator {
+ *     class = AtomicTimestampGenerator
+ *     drift-warning {
+ *       threshold = 1 second
+ *       interval = 10 seconds
+ *     }
+ *     force-java-clock = false
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class AtomicTimestampGenerator extends MonotonicTimestampGenerator { + + private AtomicLong lastRef = new AtomicLong(0); + + public AtomicTimestampGenerator(DriverContext context) { + super(context); + } + + @VisibleForTesting + AtomicTimestampGenerator(Clock clock, InternalDriverContext context) { + super(clock, context); + } + + @Override + public long next() { + while (true) { + long last = lastRef.get(); + long next = computeNext(last); + if (lastRef.compareAndSet(last, next)) { + return next; + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/time/Clock.java b/core/src/main/java/com/datastax/oss/driver/internal/core/time/Clock.java new file mode 100644 index 00000000000..4a12a788068 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/time/Clock.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.time; + +import com.datastax.oss.driver.internal.core.os.Native; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A small abstraction around system clock that aims to provide microsecond precision with the best + * accuracy possible. + */ +public interface Clock { + Logger LOG = LoggerFactory.getLogger(Clock.class); + + /** Returns the best implementation for the current platform. */ + static Clock getInstance(boolean forceJavaClock) { + if (forceJavaClock) { + LOG.info("Using Java system clock because this was explicitly required in the configuration"); + return new JavaClock(); + } else if (!Native.isCurrentTimeMicrosAvailable()) { + LOG.info( + "Could not access native clock (see debug logs for details), " + + "falling back to Java system clock"); + return new JavaClock(); + } else { + LOG.info("Using native clock for microsecond precision"); + return new NativeClock(); + } + } + + /** + * Returns the difference, measured in microseconds, between the current time and and the + * Epoch (that is, midnight, January 1, 1970 UTC). + */ + long currentTimeMicros(); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/time/JavaClock.java b/core/src/main/java/com/datastax/oss/driver/internal/core/time/JavaClock.java new file mode 100644 index 00000000000..449d298019f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/time/JavaClock.java @@ -0,0 +1,26 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.time; + +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class JavaClock implements Clock { + @Override + public long currentTimeMicros() { + return System.currentTimeMillis() * 1000; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/time/MonotonicTimestampGenerator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/time/MonotonicTimestampGenerator.java new file mode 100644 index 00000000000..9fa0bf482bf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/time/MonotonicTimestampGenerator.java @@ -0,0 +1,111 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.time; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.time.TimestampGenerator; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicLong; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A timestamp generator that guarantees monotonicity, and logs warnings when timestamps drift in + * the future. + */ +@ThreadSafe +abstract class MonotonicTimestampGenerator implements TimestampGenerator { + + private static final Logger LOG = LoggerFactory.getLogger(MonotonicTimestampGenerator.class); + + private final Clock clock; + private final long warningThresholdMicros; + private final long warningIntervalMillis; + private final AtomicLong lastDriftWarning = new AtomicLong(Long.MIN_VALUE); + + protected MonotonicTimestampGenerator(DriverContext context) { + this(buildClock(context), context); + } + + protected MonotonicTimestampGenerator(Clock clock, DriverContext context) { + this.clock = clock; + + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + this.warningThresholdMicros = + config + .getDuration( + DefaultDriverOption.TIMESTAMP_GENERATOR_DRIFT_WARNING_THRESHOLD, Duration.ZERO) + .toNanos() + / 1000; + + if (this.warningThresholdMicros == 0) { + this.warningIntervalMillis = 0; + } else { + this.warningIntervalMillis = + config + .getDuration(DefaultDriverOption.TIMESTAMP_GENERATOR_DRIFT_WARNING_INTERVAL) + .toMillis(); + } + } + + /** + * Compute the next timestamp, given the current clock tick and the last timestamp returned. + * + *

If timestamps have to drift ahead of the current clock tick to guarantee monotonicity, a + * warning will be logged according to the rules defined in the configuration. + */ + protected long computeNext(long last) { + long currentTick = clock.currentTimeMicros(); + if (last >= currentTick) { + maybeLog(currentTick, last); + return last + 1; + } + return currentTick; + } + + @Override + public void close() throws Exception { + // nothing to do + } + + private void maybeLog(long currentTick, long last) { + if (warningThresholdMicros != 0 + && LOG.isWarnEnabled() + && last > currentTick + warningThresholdMicros) { + long now = System.currentTimeMillis(); + long lastWarning = lastDriftWarning.get(); + if (now > lastWarning + warningIntervalMillis + && lastDriftWarning.compareAndSet(lastWarning, now)) { + LOG.warn( + "Clock skew detected: current tick ({}) was {} microseconds behind the last generated timestamp ({}), " + + "returned timestamps will be artificially incremented to guarantee monotonicity.", + currentTick, + last - currentTick, + last); + } + } + } + + private static Clock buildClock(DriverContext context) { + DriverExecutionProfile config = context.getConfig().getDefaultProfile(); + boolean forceJavaClock = + config.getBoolean(DefaultDriverOption.TIMESTAMP_GENERATOR_FORCE_JAVA_CLOCK, false); + return Clock.getInstance(forceJavaClock); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/time/NativeClock.java b/core/src/main/java/com/datastax/oss/driver/internal/core/time/NativeClock.java new file mode 100644 index 00000000000..7e6caf73a05 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/time/NativeClock.java @@ -0,0 +1,85 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.time; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.datastax.oss.driver.internal.core.os.Native; +import java.util.concurrent.atomic.AtomicReference; +import net.jcip.annotations.ThreadSafe; + +/** + * Provides the current time with microseconds precision with some reasonable accuracy through the + * use of {@link Native#currentTimeMicros()}. + * + *

Because calling JNR methods is slightly expensive, we only call it once per second and add the + * number of nanoseconds since the last call to get the current time, which is good enough an + * accuracy for our purpose (see CASSANDRA-6106). + * + *

This reduces the cost of the call to {@link NativeClock#currentTimeMicros()} to levels + * comparable to those of a call to {@link System#nanoTime()}. + */ +@ThreadSafe +public class NativeClock implements Clock { + + private static final long ONE_SECOND_NS = NANOSECONDS.convert(1, SECONDS); + private static final long ONE_MILLISECOND_NS = NANOSECONDS.convert(1, MILLISECONDS); + + // Records a time in micros along with the System.nanoTime() value at the time the time is + // fetched. + private static class FetchedTime { + + private final long timeInMicros; + private final long nanoTimeAtCheck; + + private FetchedTime(long timeInMicros, long nanoTimeAtCheck) { + this.timeInMicros = timeInMicros; + this.nanoTimeAtCheck = nanoTimeAtCheck; + } + } + + private final AtomicReference lastFetchedTime = + new AtomicReference<>(fetchTimeMicros()); + + @Override + public long currentTimeMicros() { + FetchedTime spec = lastFetchedTime.get(); + long curNano = System.nanoTime(); + if (curNano > spec.nanoTimeAtCheck + ONE_SECOND_NS) { + lastFetchedTime.compareAndSet(spec, spec = fetchTimeMicros()); + } + return spec.timeInMicros + ((curNano - spec.nanoTimeAtCheck) / 1000); + } + + private static FetchedTime fetchTimeMicros() { + // To compensate for the fact that the Native.currentTimeMicros call could take some time, + // instead of picking the nano time before the call or after the call, we take the average of + // both. + long start = System.nanoTime(); + long micros = Native.currentTimeMicros(); + long end = System.nanoTime(); + // If it turns out the call took us more than 1 millisecond (can happen while the JVM warms up, + // unlikely otherwise, but no reasons to take risks), fall back to System.currentTimeMillis() + // temporarily. + if ((end - start) > ONE_MILLISECOND_NS) { + return new FetchedTime(System.currentTimeMillis() * 1000, System.nanoTime()); + } else { + return new FetchedTime(micros, (end + start) / 2); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/time/ServerSideTimestampGenerator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/time/ServerSideTimestampGenerator.java new file mode 100644 index 00000000000..1e9f6c52eeb --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/time/ServerSideTimestampGenerator.java @@ -0,0 +1,55 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.time; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.time.TimestampGenerator; +import net.jcip.annotations.ThreadSafe; + +/** + * A timestamp generator that never sends a timestamp with any query, therefore letting Cassandra + * assign a server-side timestamp. + * + *

To activate this generator, modify the {@code advanced.timestamp-generator} section in the + * driver configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.timestamp-generator {
+ *     class = ServerSideTimestampGenerator
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class ServerSideTimestampGenerator implements TimestampGenerator { + + public ServerSideTimestampGenerator(@SuppressWarnings("unused") DriverContext context) { + // nothing to do + } + + @Override + public long next() { + return Long.MIN_VALUE; + } + + @Override + public void close() throws Exception { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/time/ThreadLocalTimestampGenerator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/time/ThreadLocalTimestampGenerator.java new file mode 100644 index 00000000000..511a3a2e395 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/time/ThreadLocalTimestampGenerator.java @@ -0,0 +1,69 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.time; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import net.jcip.annotations.ThreadSafe; + +/** + * A timestamp generator that guarantees monotonically increasing timestamps within each thread, and + * logs warnings when timestamps drift in the future. + * + *

Beware that there is a risk of timestamp collision with this generator when accessed by more + * than one thread at a time; only use it when threads are not in direct competition for timestamp + * ties (i.e., they are executing independent statements). + * + *

To activate this generator, modify the {@code advanced.timestamp-generator} section in the + * driver configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.timestamp-generator {
+ *     class = ThreadLocalTimestampGenerator
+ *     drift-warning {
+ *       threshold = 1 second
+ *       interval = 10 seconds
+ *     }
+ *     force-java-clock = false
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + */ +@ThreadSafe +public class ThreadLocalTimestampGenerator extends MonotonicTimestampGenerator { + + private final ThreadLocal lastRef = ThreadLocal.withInitial(() -> 0L); + + public ThreadLocalTimestampGenerator(DriverContext context) { + super(context); + } + + @VisibleForTesting + ThreadLocalTimestampGenerator(Clock clock, DriverContext context) { + super(clock, context); + } + + @Override + public long next() { + Long last = this.lastRef.get(); + long next = computeNext(last); + this.lastRef.set(next); + return next; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/tracker/NoopRequestTracker.java b/core/src/main/java/com/datastax/oss/driver/internal/core/tracker/NoopRequestTracker.java new file mode 100644 index 00000000000..bb4dca8d1ba --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/tracker/NoopRequestTracker.java @@ -0,0 +1,95 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.tracker; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.ThreadSafe; + +/** + * A no-op request tracker. + * + *

To activate this tracker, modify the {@code advanced.request-tracker} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.request-tracker {
+ *     class = NoopRequestTracker
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + * + *

Note that if a tracker is specified programmatically with {@link + * SessionBuilder#withRequestTracker(RequestTracker)}, the configuration is ignored. + */ +@ThreadSafe +public class NoopRequestTracker implements RequestTracker { + + public NoopRequestTracker(@SuppressWarnings("unused") DriverContext context) { + // nothing to do + } + + @Override + public void onSuccess( + @NonNull Request request, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node) { + // nothing to do + } + + @Override + public void onError( + @NonNull Request request, + @NonNull Throwable error, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + Node node) { + // nothing to do + } + + @Override + public void onNodeError( + @NonNull Request request, + @NonNull Throwable error, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node) { + // nothing to do + } + + @Override + public void onNodeSuccess( + @NonNull Request request, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node) { + // nothing to do + } + + @Override + public void close() throws Exception { + // nothing to do + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/tracker/RequestLogFormatter.java b/core/src/main/java/com/datastax/oss/driver/internal/core/tracker/RequestLogFormatter.java new file mode 100644 index 00000000000..fd49a2e92d9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/tracker/RequestLogFormatter.java @@ -0,0 +1,300 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.tracker; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BatchableStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.DefaultBatchType; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.internal.core.util.NanoTime; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class RequestLogFormatter { + + private static final String FURTHER_VALUES_TRUNCATED = "...]"; + private static final String TRUNCATED = "..."; + + private final DriverContext context; + + public RequestLogFormatter(DriverContext context) { + this.context = context; + } + + public StringBuilder logBuilder(String logPrefix, Node node) { + return new StringBuilder("[").append(logPrefix).append("][").append(node).append("] "); + } + + public void appendSuccessDescription(StringBuilder builder) { + builder.append("Success "); + } + + public void appendSlowDescription(StringBuilder builder) { + builder.append("Slow "); + } + + public void appendErrorDescription(StringBuilder builder) { + builder.append("Error "); + } + + public void appendLatency(long latencyNanos, StringBuilder builder) { + builder.append('(').append(NanoTime.format(latencyNanos)).append(") "); + } + + public void appendRequest( + Request request, + int maxQueryLength, + boolean showValues, + int maxValues, + int maxValueLength, + StringBuilder builder) { + appendStats(request, builder); + appendQueryString(request, maxQueryLength, builder); + if (showValues) { + appendValues(request, maxValues, maxValueLength, true, builder); + } + } + + protected void appendStats(Request request, StringBuilder builder) { + int valueCount = countBoundValues(request); + if (request instanceof BatchStatement) { + BatchStatement statement = (BatchStatement) request; + builder + .append('[') + .append(statement.size()) + .append(" statements, ") + .append(valueCount) + .append(" values] "); + } else { + builder.append('[').append(valueCount).append(" values] "); + } + } + + protected int countBoundValues(Request request) { + if (request instanceof BatchStatement) { + int count = 0; + for (BatchableStatement child : (BatchStatement) request) { + count += countBoundValues(child); + } + return count; + } else if (request instanceof BoundStatement) { + return ((BoundStatement) request).getPreparedStatement().getVariableDefinitions().size(); + } else if (request instanceof SimpleStatement) { + SimpleStatement statement = (SimpleStatement) request; + return Math.max(statement.getPositionalValues().size(), statement.getNamedValues().size()); + } else { + return 0; + } + } + + protected int appendQueryString(Request request, int limit, StringBuilder builder) { + if (request instanceof BatchStatement) { + BatchStatement batch = (BatchStatement) request; + limit = append("BEGIN", limit, builder); + if (batch.getBatchType() == DefaultBatchType.UNLOGGED) { + limit = append(" UNLOGGED", limit, builder); + } else if (batch.getBatchType() == DefaultBatchType.COUNTER) { + limit = append(" COUNTER", limit, builder); + } + limit = append(" BATCH ", limit, builder); + for (BatchableStatement child : batch) { + limit = appendQueryString(child, limit, builder); + if (limit < 0) { + break; + } + limit = append("; ", limit, builder); + } + limit = append("APPLY BATCH", limit, builder); + return limit; + } else if (request instanceof BoundStatement) { + BoundStatement statement = (BoundStatement) request; + return append(statement.getPreparedStatement().getQuery(), limit, builder); + } else if (request instanceof SimpleStatement) { + SimpleStatement statement = (SimpleStatement) request; + return append(statement.getQuery(), limit, builder); + } else { + return append(request.toString(), limit, builder); + } + } + + /** + * @return the number of values that can still be appended after this, or -1 if the max was + * reached by this call. + */ + protected int appendValues( + Request request, + int maxValues, + int maxValueLength, + boolean addSeparator, + StringBuilder builder) { + if (request instanceof BatchStatement) { + BatchStatement batch = (BatchStatement) request; + for (BatchableStatement child : batch) { + maxValues = appendValues(child, maxValues, maxValueLength, addSeparator, builder); + if (addSeparator) { + addSeparator = false; + } + if (maxValues < 0) { + return -1; + } + } + } else if (request instanceof BoundStatement) { + BoundStatement statement = (BoundStatement) request; + ColumnDefinitions definitions = statement.getPreparedStatement().getVariableDefinitions(); + List values = statement.getValues(); + assert definitions.size() == values.size(); + if (definitions.size() > 0) { + if (addSeparator) { + builder.append(' '); + } + builder.append('['); + for (int i = 0; i < definitions.size(); i++) { + if (i > 0) { + builder.append(", "); + } + maxValues -= 1; + if (maxValues < 0) { + builder.append(FURTHER_VALUES_TRUNCATED); + return -1; + } + builder.append(definitions.get(i).getName().asCql(true)).append('='); + if (!statement.isSet(i)) { + builder.append(""); + } else { + ByteBuffer value = values.get(i); + DataType type = definitions.get(i).getType(); + appendValue(value, type, maxValueLength, builder); + } + } + builder.append(']'); + } + } else if (request instanceof SimpleStatement) { + SimpleStatement statement = (SimpleStatement) request; + if (!statement.getPositionalValues().isEmpty()) { + if (addSeparator) { + builder.append(' '); + } + builder.append('['); + int i = 0; + for (Object value : statement.getPositionalValues()) { + if (i > 0) { + builder.append(", "); + } + maxValues -= 1; + if (maxValues < 0) { + builder.append(FURTHER_VALUES_TRUNCATED); + return -1; + } + builder.append('v').append(i).append('='); + appendValue(value, maxValueLength, builder); + i += 1; + } + builder.append(']'); + } else if (!statement.getNamedValues().isEmpty()) { + if (addSeparator) { + builder.append(' '); + } + builder.append('['); + int i = 0; + for (Map.Entry entry : statement.getNamedValues().entrySet()) { + if (i > 0) { + builder.append(", "); + } + maxValues -= 1; + if (maxValues < 0) { + builder.append(FURTHER_VALUES_TRUNCATED); + return -1; + } + builder.append(entry.getKey().asCql(true)).append('='); + appendValue(entry.getValue(), maxValueLength, builder); + i += 1; + } + builder.append(']'); + } + } + return maxValues; + } + + protected void appendValue(ByteBuffer raw, DataType type, int maxLength, StringBuilder builder) { + TypeCodec codec = context.getCodecRegistry().codecFor(type); + if (type.equals(DataTypes.BLOB)) { + // For very large buffers, apply the limit before converting into a string + int maxBufferLength = Math.max((maxLength - 2) / 2, 0); + boolean bufferTooLarge = raw.remaining() > maxBufferLength; + if (bufferTooLarge) { + raw = (ByteBuffer) raw.duplicate().limit(maxBufferLength); + } + Object value = codec.decode(raw, context.getProtocolVersion()); + append(codec.format(value), maxLength, builder); + if (bufferTooLarge) { + builder.append(TRUNCATED); + } + } else { + Object value = codec.decode(raw, context.getProtocolVersion()); + append(codec.format(value), maxLength, builder); + } + } + + protected void appendValue(Object value, int maxLength, StringBuilder builder) { + TypeCodec codec = context.getCodecRegistry().codecFor(value); + if (value instanceof ByteBuffer) { + // For very large buffers, apply the limit before converting into a string + ByteBuffer buffer = (ByteBuffer) value; + int maxBufferLength = Math.max((maxLength - 2) / 2, 0); + boolean bufferTooLarge = buffer.remaining() > maxBufferLength; + if (bufferTooLarge) { + buffer = (ByteBuffer) buffer.duplicate().limit(maxBufferLength); + } + append(codec.format(buffer), maxLength, builder); + if (bufferTooLarge) { + builder.append(TRUNCATED); + } + } else { + append(codec.format(value), maxLength, builder); + } + } + + /** + * @return the number of characters that can still be appended after this, or -1 if this call hit + * the limit. + */ + protected int append(String value, int limit, StringBuilder builder) { + if (limit < 0) { + // Small simplification to avoid having to check the limit every time when we do a sequence of + // simple calls, like BEGIN... UNLOGGED... BATCH. If the first call hits the limit, the next + // ones will be ignored. + return limit; + } else if (value.length() <= limit) { + builder.append(value); + return limit - value.length(); + } else { + builder.append(value.substring(0, limit)).append(TRUNCATED); + return -1; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/tracker/RequestLogger.java b/core/src/main/java/com/datastax/oss/driver/internal/core/tracker/RequestLogger.java new file mode 100644 index 00000000000..220a2222d89 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/tracker/RequestLogger.java @@ -0,0 +1,236 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.tracker; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Duration; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A request tracker that logs the requests executed through the session, according to a set of + * configurable options. + * + *

To activate this tracker, modify the {@code advanced.request-tracker} section in the driver + * configuration, for example: + * + *

+ * datastax-java-driver {
+ *   advanced.request-tracker {
+ *     class = RequestLogger
+ *     logs {
+ *       success { enabled = true }
+ *       slow { enabled = true, threshold = 1 second }
+ *       error { enabled = true }
+ *       max-query-length = 500
+ *       show-values = true
+ *       max-value-length = 50
+ *       max-values = 50
+ *       show-stack-traces = true
+ *     }
+ *   }
+ * }
+ * 
+ * + * See {@code reference.conf} (in the manual or core driver JAR) for more details. + * + *

Note that if a tracker is specified programmatically with {@link + * SessionBuilder#withRequestTracker(RequestTracker)}, the configuration is ignored. + */ +@ThreadSafe +public class RequestLogger implements RequestTracker { + + private static final Logger LOG = LoggerFactory.getLogger(RequestLogger.class); + + public static final int DEFAULT_REQUEST_LOGGER_MAX_QUERY_LENGTH = 500; + public static final boolean DEFAULT_REQUEST_LOGGER_SHOW_VALUES = true; + public static final int DEFAULT_REQUEST_LOGGER_MAX_VALUES = 50; + public static final int DEFAULT_REQUEST_LOGGER_MAX_VALUE_LENGTH = 50; + + private final String logPrefix; + private final RequestLogFormatter formatter; + + public RequestLogger(DriverContext context) { + this(context.getSessionName(), new RequestLogFormatter(context)); + } + + protected RequestLogger(String logPrefix, RequestLogFormatter formatter) { + this.logPrefix = logPrefix; + this.formatter = formatter; + } + + @Override + public void onSuccess( + @NonNull Request request, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node) { + + boolean successEnabled = + executionProfile.getBoolean(DefaultDriverOption.REQUEST_LOGGER_SUCCESS_ENABLED, false); + boolean slowEnabled = + executionProfile.getBoolean(DefaultDriverOption.REQUEST_LOGGER_SLOW_ENABLED, false); + if (!successEnabled && !slowEnabled) { + return; + } + + long slowThresholdNanos = + executionProfile + .getDuration(DefaultDriverOption.REQUEST_LOGGER_SLOW_THRESHOLD, Duration.ofSeconds(1)) + .toNanos(); + boolean isSlow = latencyNanos > slowThresholdNanos; + if ((isSlow && !slowEnabled) || (!isSlow && !successEnabled)) { + return; + } + + int maxQueryLength = + executionProfile.getInt( + DefaultDriverOption.REQUEST_LOGGER_MAX_QUERY_LENGTH, + DEFAULT_REQUEST_LOGGER_MAX_QUERY_LENGTH); + boolean showValues = + executionProfile.getBoolean( + DefaultDriverOption.REQUEST_LOGGER_VALUES, DEFAULT_REQUEST_LOGGER_SHOW_VALUES); + int maxValues = + executionProfile.getInt( + DefaultDriverOption.REQUEST_LOGGER_MAX_VALUES, DEFAULT_REQUEST_LOGGER_MAX_VALUES); + int maxValueLength = + executionProfile.getInt( + DefaultDriverOption.REQUEST_LOGGER_MAX_VALUE_LENGTH, + DEFAULT_REQUEST_LOGGER_MAX_VALUE_LENGTH); + + logSuccess( + request, latencyNanos, isSlow, node, maxQueryLength, showValues, maxValues, maxValueLength); + } + + @Override + public void onError( + @NonNull Request request, + @NonNull Throwable error, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + Node node) { + + if (!executionProfile.getBoolean(DefaultDriverOption.REQUEST_LOGGER_ERROR_ENABLED, false)) { + return; + } + + int maxQueryLength = + executionProfile.getInt( + DefaultDriverOption.REQUEST_LOGGER_MAX_QUERY_LENGTH, + DEFAULT_REQUEST_LOGGER_MAX_QUERY_LENGTH); + boolean showValues = + executionProfile.getBoolean( + DefaultDriverOption.REQUEST_LOGGER_VALUES, DEFAULT_REQUEST_LOGGER_SHOW_VALUES); + int maxValues = + executionProfile.getInt( + DefaultDriverOption.REQUEST_LOGGER_MAX_VALUES, DEFAULT_REQUEST_LOGGER_MAX_VALUES); + + int maxValueLength = + executionProfile.getInt( + DefaultDriverOption.REQUEST_LOGGER_MAX_VALUE_LENGTH, + DEFAULT_REQUEST_LOGGER_MAX_VALUE_LENGTH); + boolean showStackTraces = + executionProfile.getBoolean(DefaultDriverOption.REQUEST_LOGGER_STACK_TRACES, false); + + logError( + request, + error, + latencyNanos, + node, + maxQueryLength, + showValues, + maxValues, + maxValueLength, + showStackTraces); + } + + @Override + public void onNodeError( + @NonNull Request request, + @NonNull Throwable error, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node) { + // Nothing to do + } + + @Override + public void onNodeSuccess( + @NonNull Request request, + long latencyNanos, + @NonNull DriverExecutionProfile executionProfile, + @NonNull Node node) { + // Nothing to do + } + + @Override + public void close() throws Exception { + // nothing to do + } + + protected void logSuccess( + Request request, + long latencyNanos, + boolean isSlow, + Node node, + int maxQueryLength, + boolean showValues, + int maxValues, + int maxValueLength) { + + StringBuilder builder = formatter.logBuilder(logPrefix, node); + if (isSlow) { + formatter.appendSlowDescription(builder); + } else { + formatter.appendSuccessDescription(builder); + } + formatter.appendLatency(latencyNanos, builder); + formatter.appendRequest( + request, maxQueryLength, showValues, maxValues, maxValueLength, builder); + LOG.info(builder.toString()); + } + + protected void logError( + Request request, + Throwable error, + long latencyNanos, + Node node, + int maxQueryLength, + boolean showValues, + int maxValues, + int maxValueLength, + boolean showStackTraces) { + + StringBuilder builder = formatter.logBuilder(logPrefix, node); + formatter.appendErrorDescription(builder); + formatter.appendLatency(latencyNanos, builder); + formatter.appendRequest( + request, maxQueryLength, showValues, maxValues, maxValueLength, builder); + if (showStackTraces) { + LOG.error(builder.toString(), error); + } else { + LOG.error("{} [{}]", builder.toString(), error.toString()); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/DataTypeHelper.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DataTypeHelper.java new file mode 100644 index 00000000000..97e23446227 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DataTypeHelper.java @@ -0,0 +1,110 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.result.RawType; +import com.datastax.oss.protocol.internal.util.IntMap; +import java.util.List; +import java.util.Map; + +public class DataTypeHelper { + + public static DataType fromProtocolSpec(RawType rawType, AttachmentPoint attachmentPoint) { + DataType type = PRIMITIVE_TYPES_BY_CODE.get(rawType.id); + if (type != null) { + return type; + } else { + switch (rawType.id) { + case ProtocolConstants.DataType.CUSTOM: + RawType.RawCustom rawCustom = (RawType.RawCustom) rawType; + return DataTypes.custom(rawCustom.className); + case ProtocolConstants.DataType.LIST: + RawType.RawList rawList = (RawType.RawList) rawType; + return DataTypes.listOf(fromProtocolSpec(rawList.elementType, attachmentPoint)); + case ProtocolConstants.DataType.SET: + RawType.RawSet rawSet = (RawType.RawSet) rawType; + return DataTypes.setOf(fromProtocolSpec(rawSet.elementType, attachmentPoint)); + case ProtocolConstants.DataType.MAP: + RawType.RawMap rawMap = (RawType.RawMap) rawType; + return DataTypes.mapOf( + fromProtocolSpec(rawMap.keyType, attachmentPoint), + fromProtocolSpec(rawMap.valueType, attachmentPoint)); + case ProtocolConstants.DataType.TUPLE: + RawType.RawTuple rawTuple = (RawType.RawTuple) rawType; + List rawFieldsList = rawTuple.fieldTypes; + ImmutableList.Builder fields = ImmutableList.builder(); + for (RawType rawField : rawFieldsList) { + fields.add(fromProtocolSpec(rawField, attachmentPoint)); + } + return new DefaultTupleType(fields.build(), attachmentPoint); + case ProtocolConstants.DataType.UDT: + RawType.RawUdt rawUdt = (RawType.RawUdt) rawType; + ImmutableList.Builder fieldNames = ImmutableList.builder(); + ImmutableList.Builder fieldTypes = ImmutableList.builder(); + for (Map.Entry entry : rawUdt.fields.entrySet()) { + fieldNames.add(CqlIdentifier.fromInternal(entry.getKey())); + fieldTypes.add(fromProtocolSpec(entry.getValue(), attachmentPoint)); + } + return new DefaultUserDefinedType( + CqlIdentifier.fromInternal(rawUdt.keyspace), + CqlIdentifier.fromInternal(rawUdt.typeName), + false, + fieldNames.build(), + fieldTypes.build(), + attachmentPoint); + default: + throw new IllegalArgumentException("Unsupported type: " + rawType.id); + } + } + } + + private static IntMap PRIMITIVE_TYPES_BY_CODE = + sortByProtocolCode( + DataTypes.ASCII, + DataTypes.BIGINT, + DataTypes.BLOB, + DataTypes.BOOLEAN, + DataTypes.COUNTER, + DataTypes.DECIMAL, + DataTypes.DOUBLE, + DataTypes.FLOAT, + DataTypes.INT, + DataTypes.TIMESTAMP, + DataTypes.UUID, + DataTypes.VARINT, + DataTypes.TIMEUUID, + DataTypes.INET, + DataTypes.DATE, + DataTypes.TEXT, + DataTypes.TIME, + DataTypes.SMALLINT, + DataTypes.TINYINT, + DataTypes.DURATION); + + private static IntMap sortByProtocolCode(DataType... types) { + IntMap.Builder builder = IntMap.builder(); + for (DataType type : types) { + builder.put(type.getProtocolCode(), type); + } + return builder.build(); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultCustomType.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultCustomType.java new file mode 100644 index 00000000000..3b2f22ff302 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultCustomType.java @@ -0,0 +1,82 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.CustomType; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultCustomType implements CustomType, Serializable { + + private static final long serialVersionUID = 1; + + /** @serial */ + private final String className; + + public DefaultCustomType(@NonNull String className) { + Preconditions.checkNotNull(className); + this.className = className; + } + + @NonNull + @Override + public String getClassName() { + return className; + } + + @Override + public boolean isDetached() { + return false; + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + // nothing to do + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof CustomType) { + CustomType that = (CustomType) other; + return this.className.equals(that.getClassName()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return className.hashCode(); + } + + @Override + public String toString() { + return "Custom(" + className + ")"; + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + Preconditions.checkNotNull(className); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultListType.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultListType.java new file mode 100644 index 00000000000..f8b93ed5696 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultListType.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultListType implements ListType, Serializable { + + private static final long serialVersionUID = 1; + + /** @serial */ + private final DataType elementType; + /** @serial */ + private final boolean frozen; + + public DefaultListType(@NonNull DataType elementType, boolean frozen) { + Preconditions.checkNotNull(elementType); + this.elementType = elementType; + this.frozen = frozen; + } + + @NonNull + @Override + public DataType getElementType() { + return elementType; + } + + @Override + public boolean isFrozen() { + return frozen; + } + + @Override + public boolean isDetached() { + return elementType.isDetached(); + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + elementType.attach(attachmentPoint); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof ListType) { + ListType that = (ListType) other; + // frozen is not taken into account + return this.elementType.equals(that.getElementType()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(DefaultListType.class, this.elementType); + } + + @Override + public String toString() { + return "List(" + elementType + ", " + (frozen ? "" : "not ") + "frozen)"; + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + Preconditions.checkNotNull(elementType); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultMapType.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultMapType.java new file mode 100644 index 00000000000..316975484e2 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultMapType.java @@ -0,0 +1,105 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.MapType; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultMapType implements MapType, Serializable { + + private static final long serialVersionUID = 1; + + /** @serial */ + private final DataType keyType; + /** @serial */ + private final DataType valueType; + /** @serial */ + private final boolean frozen; + + public DefaultMapType(@NonNull DataType keyType, @NonNull DataType valueType, boolean frozen) { + Preconditions.checkNotNull(keyType); + Preconditions.checkNotNull(valueType); + this.keyType = keyType; + this.valueType = valueType; + this.frozen = frozen; + } + + @NonNull + @Override + public DataType getKeyType() { + return keyType; + } + + @NonNull + @Override + public DataType getValueType() { + return valueType; + } + + @Override + public boolean isFrozen() { + return frozen; + } + + @Override + public boolean isDetached() { + return keyType.isDetached() || valueType.isDetached(); + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + keyType.attach(attachmentPoint); + valueType.attach(attachmentPoint); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof MapType) { + MapType that = (MapType) other; + // frozen is not taken into account + return this.keyType.equals(that.getKeyType()) && this.valueType.equals(that.getValueType()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(DefaultMapType.class, keyType, valueType); + } + + @Override + public String toString() { + return "Map(" + keyType + " => " + valueType + ", " + (frozen ? "" : "not ") + "frozen)"; + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + Preconditions.checkNotNull(keyType); + Preconditions.checkNotNull(valueType); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultSetType.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultSetType.java new file mode 100644 index 00000000000..285315a6b20 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultSetType.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultSetType implements SetType, Serializable { + + private static final long serialVersionUID = 1; + + /** @serial */ + private final DataType elementType; + /** @serial */ + private final boolean frozen; + + public DefaultSetType(@NonNull DataType elementType, boolean frozen) { + Preconditions.checkNotNull(elementType); + this.elementType = elementType; + this.frozen = frozen; + } + + @NonNull + @Override + public DataType getElementType() { + return elementType; + } + + @Override + public boolean isFrozen() { + return frozen; + } + + @Override + public boolean isDetached() { + return elementType.isDetached(); + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + elementType.attach(attachmentPoint); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof SetType) { + SetType that = (SetType) other; + // frozen is not taken into account + return this.elementType.equals(that.getElementType()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(DefaultSetType.class, this.elementType); + } + + @Override + public String toString() { + return "Set(" + elementType + ", " + (frozen ? "" : "not ") + "frozen)"; + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + Preconditions.checkNotNull(elementType); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultTupleType.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultTupleType.java new file mode 100644 index 00000000000..efecfdf540e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultTupleType.java @@ -0,0 +1,120 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.internal.core.data.DefaultTupleValue; +import com.datastax.oss.driver.shaded.guava.common.base.Joiner; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.List; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultTupleType implements TupleType, Serializable { + + private static final long serialVersionUID = 1; + + /** @serial */ + private final ImmutableList componentTypes; + + private transient volatile AttachmentPoint attachmentPoint; + + public DefaultTupleType( + @NonNull List componentTypes, @NonNull AttachmentPoint attachmentPoint) { + Preconditions.checkNotNull(componentTypes); + this.componentTypes = ImmutableList.copyOf(componentTypes); + this.attachmentPoint = attachmentPoint; + } + + public DefaultTupleType(@NonNull List componentTypes) { + this(componentTypes, AttachmentPoint.NONE); + } + + @NonNull + @Override + public List getComponentTypes() { + return componentTypes; + } + + @NonNull + @Override + public TupleValue newValue() { + return new DefaultTupleValue(this); + } + + @NonNull + @Override + public TupleValue newValue(@NonNull Object... values) { + return new DefaultTupleValue(this, values); + } + + @Override + public boolean isDetached() { + return attachmentPoint == AttachmentPoint.NONE; + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + this.attachmentPoint = attachmentPoint; + for (DataType componentType : componentTypes) { + componentType.attach(attachmentPoint); + } + } + + @NonNull + @Override + public AttachmentPoint getAttachmentPoint() { + return attachmentPoint; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof TupleType) { + TupleType that = (TupleType) other; + return this.componentTypes.equals(that.getComponentTypes()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return componentTypes.hashCode(); + } + + @Override + public String toString() { + return "Tuple(" + WITH_COMMA.join(componentTypes) + ")"; + } + + private static final Joiner WITH_COMMA = Joiner.on(", "); + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + Preconditions.checkNotNull(componentTypes); + this.attachmentPoint = AttachmentPoint.NONE; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultUserDefinedType.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultUserDefinedType.java new file mode 100644 index 00000000000..92cfe72fe14 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/DefaultUserDefinedType.java @@ -0,0 +1,206 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.data.DefaultUdtValue; +import com.datastax.oss.driver.internal.core.data.IdentifierIndex; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultUserDefinedType implements UserDefinedType, Serializable { + + private static final long serialVersionUID = 1; + + /** @serial */ + private final CqlIdentifier keyspace; + /** @serial */ + private final CqlIdentifier name; + + // Data types are only [de]serialized as part of a row, frozenness doesn't matter in that context + private final transient boolean frozen; + + /** @serial */ + private final List fieldNames; + /** @serial */ + private final List fieldTypes; + + private transient IdentifierIndex index; + private transient volatile AttachmentPoint attachmentPoint; + + public DefaultUserDefinedType( + @NonNull CqlIdentifier keyspace, + @NonNull CqlIdentifier name, + boolean frozen, + List fieldNames, + @NonNull List fieldTypes, + @NonNull AttachmentPoint attachmentPoint) { + Preconditions.checkNotNull(keyspace); + Preconditions.checkNotNull(name); + Preconditions.checkNotNull(fieldNames); + Preconditions.checkNotNull(fieldTypes); + Preconditions.checkArgument(fieldNames.size() > 0, "Field names list can't be null or empty"); + Preconditions.checkArgument( + fieldTypes.size() == fieldNames.size(), + "There should be the same number of field names and types"); + this.keyspace = keyspace; + this.name = name; + this.frozen = frozen; + this.fieldNames = ImmutableList.copyOf(fieldNames); + this.fieldTypes = ImmutableList.copyOf(fieldTypes); + this.index = new IdentifierIndex(this.fieldNames); + this.attachmentPoint = attachmentPoint; + } + + public DefaultUserDefinedType( + @NonNull CqlIdentifier keyspace, + @NonNull CqlIdentifier name, + boolean frozen, + @NonNull List fieldNames, + @NonNull List fieldTypes) { + this(keyspace, name, frozen, fieldNames, fieldTypes, AttachmentPoint.NONE); + } + + @NonNull + @Override + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + @Override + public CqlIdentifier getName() { + return name; + } + + @Override + public boolean isFrozen() { + return frozen; + } + + @NonNull + @Override + public List getFieldNames() { + return fieldNames; + } + + @Override + public int firstIndexOf(@NonNull CqlIdentifier id) { + return index.firstIndexOf(id); + } + + @Override + public int firstIndexOf(@NonNull String name) { + return index.firstIndexOf(name); + } + + @NonNull + @Override + public List getFieldTypes() { + return fieldTypes; + } + + @NonNull + @Override + public UserDefinedType copy(boolean newFrozen) { + return (newFrozen == frozen) + ? this + : new DefaultUserDefinedType( + keyspace, name, newFrozen, fieldNames, fieldTypes, attachmentPoint); + } + + @NonNull + @Override + public UdtValue newValue() { + return new DefaultUdtValue(this); + } + + @NonNull + @Override + public UdtValue newValue(@NonNull Object... fields) { + return new DefaultUdtValue(this, fields); + } + + @Override + public boolean isDetached() { + return attachmentPoint == AttachmentPoint.NONE; + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + this.attachmentPoint = attachmentPoint; + for (DataType fieldType : fieldTypes) { + fieldType.attach(attachmentPoint); + } + } + + @NonNull + @Override + public AttachmentPoint getAttachmentPoint() { + return attachmentPoint; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof UserDefinedType) { + UserDefinedType that = (UserDefinedType) other; + // frozen is ignored in comparisons + return this.keyspace.equals(that.getKeyspace()) + && this.name.equals(that.getName()) + && this.fieldNames.equals(that.getFieldNames()) + && this.fieldTypes.equals(that.getFieldTypes()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(keyspace, name, fieldNames, fieldTypes); + } + + @Override + public String toString() { + return "UDT(" + keyspace.asCql(true) + "." + name.asCql(true) + ")"; + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + Preconditions.checkNotNull(keyspace); + Preconditions.checkNotNull(name); + Preconditions.checkArgument( + fieldNames != null && fieldNames.size() > 0, "Field names list can't be null or empty"); + Preconditions.checkArgument( + fieldTypes != null && fieldTypes.size() == fieldNames.size(), + "There should be the same number of field names and types"); + this.attachmentPoint = AttachmentPoint.NONE; + this.index = new IdentifierIndex(this.fieldNames); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/PrimitiveType.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/PrimitiveType.java new file mode 100644 index 00000000000..909a58d053a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/PrimitiveType.java @@ -0,0 +1,125 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.Serializable; +import net.jcip.annotations.Immutable; + +@Immutable +public class PrimitiveType implements DataType, Serializable { + + /** @serial */ + private final int protocolCode; + + public PrimitiveType(int protocolCode) { + this.protocolCode = protocolCode; + } + + @Override + public int getProtocolCode() { + return protocolCode; + } + + @Override + public boolean isDetached() { + return false; + } + + @Override + public void attach(@NonNull AttachmentPoint attachmentPoint) { + // nothing to do + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof PrimitiveType) { + PrimitiveType that = (PrimitiveType) other; + return this.protocolCode == that.protocolCode; + } else { + return false; + } + } + + @Override + public int hashCode() { + return protocolCode; + } + + @NonNull + @Override + public String asCql(boolean includeFrozen, boolean pretty) { + return codeName(protocolCode).toLowerCase(); + } + + @Override + public String toString() { + return codeName(protocolCode); + } + + private static String codeName(int protocolCode) { + // Reminder: we don't use enums to leave the door open for custom extensions + switch (protocolCode) { + case ProtocolConstants.DataType.ASCII: + return "ASCII"; + case ProtocolConstants.DataType.BIGINT: + return "BIGINT"; + case ProtocolConstants.DataType.BLOB: + return "BLOB"; + case ProtocolConstants.DataType.BOOLEAN: + return "BOOLEAN"; + case ProtocolConstants.DataType.COUNTER: + return "COUNTER"; + case ProtocolConstants.DataType.DECIMAL: + return "DECIMAL"; + case ProtocolConstants.DataType.DOUBLE: + return "DOUBLE"; + case ProtocolConstants.DataType.FLOAT: + return "FLOAT"; + case ProtocolConstants.DataType.INT: + return "INT"; + case ProtocolConstants.DataType.TIMESTAMP: + return "TIMESTAMP"; + case ProtocolConstants.DataType.UUID: + return "UUID"; + case ProtocolConstants.DataType.VARINT: + return "VARINT"; + case ProtocolConstants.DataType.TIMEUUID: + return "TIMEUUID"; + case ProtocolConstants.DataType.INET: + return "INET"; + case ProtocolConstants.DataType.DATE: + return "DATE"; + case ProtocolConstants.DataType.VARCHAR: + return "TEXT"; + case ProtocolConstants.DataType.TIME: + return "TIME"; + case ProtocolConstants.DataType.SMALLINT: + return "SMALLINT"; + case ProtocolConstants.DataType.TINYINT: + return "TINYINT"; + case ProtocolConstants.DataType.DURATION: + return "DURATION"; + default: + return "0x" + Integer.toHexString(protocolCode); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/UserDefinedTypeBuilder.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/UserDefinedTypeBuilder.java new file mode 100644 index 00000000000..1bd04ad005d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/UserDefinedTypeBuilder.java @@ -0,0 +1,76 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import net.jcip.annotations.NotThreadSafe; + +/** + * Helper class to build {@link UserDefinedType} instances. + * + *

This is not part of the public API, because building user defined types manually can be + * tricky: the fields must be defined in the exact same order as the database definition, otherwise + * you will insert corrupt data in your database. If you decide to use this class anyway, make sure + * that you define fields in the correct order, and that the database schema never changes. + */ +@NotThreadSafe +public class UserDefinedTypeBuilder { + + private final CqlIdentifier keyspaceName; + private final CqlIdentifier typeName; + private boolean frozen; + private final ImmutableList.Builder fieldNames; + private final ImmutableList.Builder fieldTypes; + + public UserDefinedTypeBuilder(CqlIdentifier keyspaceName, CqlIdentifier typeName) { + this.keyspaceName = keyspaceName; + this.typeName = typeName; + this.fieldNames = ImmutableList.builder(); + this.fieldTypes = ImmutableList.builder(); + } + + public UserDefinedTypeBuilder(String keyspaceName, String typeName) { + this(CqlIdentifier.fromCql(keyspaceName), CqlIdentifier.fromCql(typeName)); + } + + /** + * Adds a new field. The fields in the resulting type will be in the order of the calls to this + * method. + */ + public UserDefinedTypeBuilder withField(CqlIdentifier name, DataType type) { + fieldNames.add(name); + fieldTypes.add(type); + return this; + } + + public UserDefinedTypeBuilder withField(String name, DataType type) { + return withField(CqlIdentifier.fromCql(name), type); + } + + /** Makes the type frozen (by default, it is not). */ + public UserDefinedTypeBuilder frozen() { + this.frozen = true; + return this; + } + + public UserDefinedType build() { + return new DefaultUserDefinedType( + keyspaceName, typeName, frozen, fieldNames.build(), fieldTypes.build()); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/BigIntCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/BigIntCodec.java new file mode 100644 index 00000000000..7ba465543bf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/BigIntCodec.java @@ -0,0 +1,91 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveLongCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class BigIntCodec implements PrimitiveLongCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.LONG; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.BIGINT; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof Long; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == Long.class || javaClass == long.class; + } + + @Nullable + @Override + public ByteBuffer encodePrimitive(long value, @NonNull ProtocolVersion protocolVersion) { + ByteBuffer bytes = ByteBuffer.allocate(8); + bytes.putLong(0, value); + return bytes; + } + + @Override + public long decodePrimitive( + @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return 0; + } else if (bytes.remaining() != 8) { + throw new IllegalArgumentException( + "Invalid 64-bits long value, expecting 8 bytes but got " + bytes.remaining()); + } else { + return bytes.getLong(bytes.position()); + } + } + + @NonNull + @Override + public String format(@Nullable Long value) { + return (value == null) ? "NULL" : Long.toString(value); + } + + @Nullable + @Override + public Long parse(@Nullable String value) { + try { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : Long.parseLong(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse 64-bits long value from \"%s\"", value)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/BlobCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/BlobCodec.java new file mode 100644 index 00000000000..4aeed77b00b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/BlobCodec.java @@ -0,0 +1,78 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class BlobCodec implements TypeCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.BYTE_BUFFER; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.BLOB; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof ByteBuffer; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return ByteBuffer.class.equals(javaClass); + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable ByteBuffer value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : value.duplicate(); + } + + @Nullable + @Override + public ByteBuffer decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null) ? null : bytes.duplicate(); + } + + @NonNull + @Override + public String format(@Nullable ByteBuffer value) { + return (value == null) ? "NULL" : Bytes.toHexString(value); + } + + @Nullable + @Override + public ByteBuffer parse(@Nullable String value) { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : Bytes.fromHexString(value); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/BooleanCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/BooleanCodec.java new file mode 100644 index 00000000000..8768311f2ac --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/BooleanCodec.java @@ -0,0 +1,99 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveBooleanCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class BooleanCodec implements PrimitiveBooleanCodec { + + private static final ByteBuffer TRUE = ByteBuffer.wrap(new byte[] {1}); + private static final ByteBuffer FALSE = ByteBuffer.wrap(new byte[] {0}); + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.BOOLEAN; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.BOOLEAN; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof Boolean; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == Boolean.class || javaClass == boolean.class; + } + + @Nullable + @Override + public ByteBuffer encodePrimitive(boolean value, @NonNull ProtocolVersion protocolVersion) { + return value ? TRUE.duplicate() : FALSE.duplicate(); + } + + @Override + public boolean decodePrimitive( + @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return false; + } else if (bytes.remaining() != 1) { + throw new IllegalArgumentException( + "Invalid boolean value, expecting 1 byte but got " + bytes.remaining()); + } else { + return bytes.get(bytes.position()) != 0; + } + } + + @NonNull + @Override + public String format(@Nullable Boolean value) { + if (value == null) { + return "NULL"; + } else { + return value ? "true" : "false"; + } + } + + @Nullable + @Override + public Boolean parse(@Nullable String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) { + return null; + } else if (value.equalsIgnoreCase(Boolean.FALSE.toString())) { + return false; + } else if (value.equalsIgnoreCase(Boolean.TRUE.toString())) { + return true; + } else { + throw new IllegalArgumentException( + String.format("Cannot parse boolean value from \"%s\"", value)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/CounterCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/CounterCodec.java new file mode 100644 index 00000000000..3d0347eebe6 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/CounterCodec.java @@ -0,0 +1,30 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class CounterCodec extends BigIntCodec { + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.COUNTER; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/CqlDurationCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/CqlDurationCodec.java new file mode 100644 index 00000000000..0a1a49e7016 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/CqlDurationCodec.java @@ -0,0 +1,116 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.CqlDuration; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.type.util.VIntCoding; +import com.datastax.oss.driver.shaded.guava.common.io.ByteArrayDataOutput; +import com.datastax.oss.driver.shaded.guava.common.io.ByteStreams; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.io.DataInput; +import java.io.IOException; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class CqlDurationCodec implements TypeCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.CQL_DURATION; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.DURATION; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof CqlDuration; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == CqlDuration.class; + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable CqlDuration value, @NonNull ProtocolVersion protocolVersion) { + if (value == null) { + return null; + } + long months = value.getMonths(); + long days = value.getDays(); + long nanoseconds = value.getNanoseconds(); + int size = + VIntCoding.computeVIntSize(months) + + VIntCoding.computeVIntSize(days) + + VIntCoding.computeVIntSize(nanoseconds); + ByteArrayDataOutput out = ByteStreams.newDataOutput(size); + try { + VIntCoding.writeVInt(months, out); + VIntCoding.writeVInt(days, out); + VIntCoding.writeVInt(nanoseconds, out); + } catch (IOException e) { + // cannot happen + throw new AssertionError(); + } + return ByteBuffer.wrap(out.toByteArray()); + } + + @Nullable + @Override + public CqlDuration decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return null; + } else { + DataInput in = ByteStreams.newDataInput(Bytes.getArray(bytes)); + try { + int months = (int) VIntCoding.readVInt(in); + int days = (int) VIntCoding.readVInt(in); + long nanoseconds = VIntCoding.readVInt(in); + return CqlDuration.newInstance(months, days, nanoseconds); + } catch (IOException e) { + // cannot happen + throw new AssertionError(); + } + } + } + + @NonNull + @Override + public String format(@Nullable CqlDuration value) { + return (value == null) ? "NULL" : value.toString(); + } + + @Nullable + @Override + public CqlDuration parse(@Nullable String value) { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : CqlDuration.from(value); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/CustomCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/CustomCodec.java new file mode 100644 index 00000000000..b1e1d204bfd --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/CustomCodec.java @@ -0,0 +1,85 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.CustomType; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class CustomCodec implements TypeCodec { + + private final CustomType cqlType; + + public CustomCodec(CustomType cqlType) { + this.cqlType = cqlType; + } + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.BYTE_BUFFER; + } + + @NonNull + @Override + public DataType getCqlType() { + return cqlType; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof ByteBuffer; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return ByteBuffer.class.equals(javaClass); + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable ByteBuffer value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : value.duplicate(); + } + + @Nullable + @Override + public ByteBuffer decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null) ? null : bytes.duplicate(); + } + + @NonNull + @Override + public String format(@Nullable ByteBuffer value) { + return (value == null) ? "NULL" : Bytes.toHexString(value); + } + + @Nullable + @Override + public ByteBuffer parse(@Nullable String value) { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : Bytes.fromHexString(value); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/DateCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/DateCodec.java new file mode 100644 index 00000000000..82c11e73dc4 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/DateCodec.java @@ -0,0 +1,153 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static java.lang.Long.parseLong; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.util.Strings; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DateCodec implements TypeCodec { + + private static final LocalDate EPOCH = LocalDate.of(1970, 1, 1); + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.LOCAL_DATE; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.DATE; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof LocalDate; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == LocalDate.class; + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable LocalDate value, @NonNull ProtocolVersion protocolVersion) { + if (value == null) { + return null; + } + long days = ChronoUnit.DAYS.between(EPOCH, value); + int unsigned = signedToUnsigned((int) days); + return TypeCodecs.INT.encodePrimitive(unsigned, protocolVersion); + } + + @Nullable + @Override + public LocalDate decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return null; + } + int unsigned = TypeCodecs.INT.decodePrimitive(bytes, protocolVersion); + int signed = unsignedToSigned(unsigned); + return EPOCH.plusDays(signed); + } + + @NonNull + @Override + public String format(@Nullable LocalDate value) { + return (value == null) ? "NULL" : Strings.quote(DateTimeFormatter.ISO_LOCAL_DATE.format(value)); + } + + @Nullable + @Override + public LocalDate parse(@Nullable String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) { + return null; + } + + // single quotes are optional for long literals, mandatory for date patterns + // strip enclosing single quotes, if any + if (Strings.isQuoted(value)) { + value = Strings.unquote(value); + } + + if (Strings.isLongLiteral(value)) { + long raw; + try { + raw = parseLong(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse date value from \"%s\"", value)); + } + int days; + try { + days = cqlDateToDaysSinceEpoch(raw); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format("Cannot parse date value from \"%s\"", value)); + } + return EPOCH.plusDays(days); + } + + try { + return LocalDate.parse(value, DateTimeFormatter.ISO_LOCAL_DATE); + } catch (RuntimeException e) { + throw new IllegalArgumentException( + String.format("Cannot parse date value from \"%s\"", value)); + } + } + + private static int signedToUnsigned(int signed) { + return signed - Integer.MIN_VALUE; + } + + private static int unsignedToSigned(int unsigned) { + return unsigned + Integer.MIN_VALUE; // this relies on overflow for "negative" values + } + + /** + * Converts a raw CQL long representing a numeric DATE literal to the number of days since the + * Epoch. In CQL, numeric DATE literals are longs (unsigned integers actually) between 0 and 2^32 + * - 1, with the epoch in the middle; this method re-centers the epoch at 0. + */ + private static int cqlDateToDaysSinceEpoch(long raw) { + if (raw < 0 || raw > MAX_CQL_LONG_VALUE) + throw new IllegalArgumentException( + String.format( + "Numeric literals for DATE must be between 0 and %d (got %d)", + MAX_CQL_LONG_VALUE, raw)); + return (int) (raw - EPOCH_AS_CQL_LONG); + } + + private static final long MAX_CQL_LONG_VALUE = ((1L << 32) - 1); + private static final long EPOCH_AS_CQL_LONG = (1L << 31); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/DecimalCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/DecimalCodec.java new file mode 100644 index 00000000000..49a128e423d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/DecimalCodec.java @@ -0,0 +1,108 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DecimalCodec implements TypeCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.BIG_DECIMAL; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.DECIMAL; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof BigDecimal; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return BigDecimal.class.isAssignableFrom(javaClass); + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable BigDecimal value, @NonNull ProtocolVersion protocolVersion) { + if (value == null) { + return null; + } + BigInteger bi = value.unscaledValue(); + int scale = value.scale(); + byte[] bibytes = bi.toByteArray(); + + ByteBuffer bytes = ByteBuffer.allocate(4 + bibytes.length); + bytes.putInt(scale); + bytes.put(bibytes); + bytes.rewind(); + return bytes; + } + + @Nullable + @Override + public BigDecimal decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return null; + } else if (bytes.remaining() < 4) { + throw new IllegalArgumentException( + "Invalid decimal value, expecting at least 4 bytes but got " + bytes.remaining()); + } + + bytes = bytes.duplicate(); + int scale = bytes.getInt(); + byte[] bibytes = new byte[bytes.remaining()]; + bytes.get(bibytes); + + BigInteger bi = new BigInteger(bibytes); + return new BigDecimal(bi, scale); + } + + @NonNull + @Override + public String format(@Nullable BigDecimal value) { + return (value == null) ? "NULL" : value.toString(); + } + + @Nullable + @Override + public BigDecimal parse(@Nullable String value) { + try { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : new BigDecimal(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse decimal value from \"%s\"", value)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/DoubleCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/DoubleCodec.java new file mode 100644 index 00000000000..91071c0d847 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/DoubleCodec.java @@ -0,0 +1,91 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveDoubleCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class DoubleCodec implements PrimitiveDoubleCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.DOUBLE; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.DOUBLE; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof Double; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == Double.class || javaClass == double.class; + } + + @Nullable + @Override + public ByteBuffer encodePrimitive(double value, @NonNull ProtocolVersion protocolVersion) { + ByteBuffer bytes = ByteBuffer.allocate(8); + bytes.putDouble(0, value); + return bytes; + } + + @Override + public double decodePrimitive( + @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return 0; + } else if (bytes.remaining() != 8) { + throw new IllegalArgumentException( + "Invalid 64-bits double value, expecting 8 bytes but got " + bytes.remaining()); + } else { + return bytes.getDouble(bytes.position()); + } + } + + @NonNull + @Override + public String format(@Nullable Double value) { + return (value == null) ? "NULL" : Double.toString(value); + } + + @Nullable + @Override + public Double parse(@Nullable String value) { + try { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : Double.parseDouble(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse 64-bits double value from \"%s\"", value)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/FloatCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/FloatCodec.java new file mode 100644 index 00000000000..a3ff38a2b83 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/FloatCodec.java @@ -0,0 +1,91 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveFloatCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class FloatCodec implements PrimitiveFloatCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.FLOAT; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.FLOAT; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof Float; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == Float.class || javaClass == float.class; + } + + @Nullable + @Override + public ByteBuffer encodePrimitive(float value, @NonNull ProtocolVersion protocolVersion) { + ByteBuffer bytes = ByteBuffer.allocate(4); + bytes.putFloat(0, value); + return bytes; + } + + @Override + public float decodePrimitive( + @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return 0; + } else if (bytes.remaining() != 4) { + throw new IllegalArgumentException( + "Invalid 32-bits float value, expecting 4 bytes but got " + bytes.remaining()); + } else { + return bytes.getFloat(bytes.position()); + } + } + + @NonNull + @Override + public String format(@Nullable Float value) { + return (value == null) ? "NULL" : Float.toString(value); + } + + @Nullable + @Override + public Float parse(@Nullable String value) { + try { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : Float.parseFloat(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse 32-bits float value from \"%s\"", value)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/InetCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/InetCodec.java new file mode 100644 index 00000000000..efc7c254d21 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/InetCodec.java @@ -0,0 +1,101 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.util.Strings; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class InetCodec implements TypeCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.INET_ADDRESS; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.INET; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof InetAddress; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return InetAddress.class.equals(javaClass); + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable InetAddress value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : ByteBuffer.wrap(value.getAddress()); + } + + @Nullable + @Override + public InetAddress decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return null; + } + try { + return InetAddress.getByAddress(Bytes.getArray(bytes)); + } catch (UnknownHostException e) { + throw new IllegalArgumentException( + "Invalid bytes for inet value, got " + bytes.remaining() + " bytes"); + } + } + + @NonNull + @Override + public String format(@Nullable InetAddress value) { + return (value == null) ? "NULL" : ("'" + value.getHostAddress() + "'"); + } + + @Nullable + @Override + public InetAddress parse(@Nullable String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) { + return null; + } + + value = value.trim(); + if (!Strings.isQuoted(value)) { + throw new IllegalArgumentException( + String.format("inet values must be enclosed in single quotes (\"%s\")", value)); + } + try { + return InetAddress.getByName(value.substring(1, value.length() - 1)); + } catch (Exception e) { + throw new IllegalArgumentException( + String.format("Cannot parse inet value from \"%s\"", value)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/IntCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/IntCodec.java new file mode 100644 index 00000000000..702c9a40d2d --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/IntCodec.java @@ -0,0 +1,91 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveIntCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class IntCodec implements PrimitiveIntCodec { + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.INTEGER; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.INT; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof Integer; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == Integer.class || javaClass == int.class; + } + + @Nullable + @Override + public ByteBuffer encodePrimitive(int value, @NonNull ProtocolVersion protocolVersion) { + ByteBuffer bytes = ByteBuffer.allocate(4); + bytes.putInt(0, value); + return bytes; + } + + @Override + public int decodePrimitive(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return 0; + } else if (bytes.remaining() != 4) { + throw new IllegalArgumentException( + "Invalid 32-bits integer value, expecting 4 bytes but got " + bytes.remaining()); + } else { + return bytes.getInt(bytes.position()); + } + } + + @NonNull + @Override + public String format(@Nullable Integer value) { + return (value == null) ? "NULL" : Integer.toString(value); + } + + @Nullable + @Override + public Integer parse(@Nullable String value) { + try { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : Integer.parseInt(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse 32-bits int value from \"%s\"", value)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/ListCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/ListCodec.java new file mode 100644 index 00000000000..dd4001e3930 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/ListCodec.java @@ -0,0 +1,195 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class ListCodec implements TypeCodec> { + + private final DataType cqlType; + private final GenericType> javaType; + private final TypeCodec elementCodec; + + public ListCodec(DataType cqlType, TypeCodec elementCodec) { + this.cqlType = cqlType; + this.javaType = GenericType.listOf(elementCodec.getJavaType()); + this.elementCodec = elementCodec; + Preconditions.checkArgument(cqlType instanceof ListType); + } + + @NonNull + @Override + public GenericType> getJavaType() { + return javaType; + } + + @NonNull + @Override + public DataType getCqlType() { + return cqlType; + } + + @Override + public boolean accepts(@NonNull Object value) { + if (List.class.isAssignableFrom(value.getClass())) { + // runtime type ok, now check element type + List list = (List) value; + return list.isEmpty() || elementCodec.accepts(list.get(0)); + } else { + return false; + } + } + + @Nullable + @Override + public ByteBuffer encode( + @Nullable List value, @NonNull ProtocolVersion protocolVersion) { + // An int indicating the number of elements in the list, followed by the elements. Each element + // is a byte array representing the serialized value, preceded by an int indicating its size. + if (value == null) { + return null; + } else { + int i = 0; + ByteBuffer[] encodedElements = new ByteBuffer[value.size()]; + int toAllocate = 4; // initialize with number of elements + for (ElementT element : value) { + if (element == null) { + throw new NullPointerException("Collection elements cannot be null"); + } + ByteBuffer encodedElement; + try { + encodedElement = elementCodec.encode(element, protocolVersion); + } catch (ClassCastException e) { + throw new IllegalArgumentException("Invalid type for element: " + element.getClass()); + } + if (encodedElement == null) { + throw new NullPointerException("Collection elements cannot encode to CQL NULL"); + } + encodedElements[i++] = encodedElement; + toAllocate += 4 + encodedElement.remaining(); // the element preceded by its size + } + ByteBuffer result = ByteBuffer.allocate(toAllocate); + result.putInt(value.size()); + for (ByteBuffer encodedElement : encodedElements) { + result.putInt(encodedElement.remaining()); + result.put(encodedElement); + } + result.flip(); + return result; + } + } + + @Nullable + @Override + public List decode( + @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return new ArrayList<>(0); + } else { + ByteBuffer input = bytes.duplicate(); + int size = input.getInt(); + List result = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + int elementSize = input.getInt(); + ByteBuffer encodedElement = input.slice(); + encodedElement.limit(elementSize); + input.position(input.position() + elementSize); + result.add(elementCodec.decode(encodedElement, protocolVersion)); + } + return result; + } + } + + @NonNull + @Override + public String format(@Nullable List value) { + if (value == null) { + return "NULL"; + } + StringBuilder sb = new StringBuilder("["); + boolean first = true; + for (ElementT t : value) { + if (first) { + first = false; + } else { + sb.append(","); + } + sb.append(elementCodec.format(t)); + } + sb.append("]"); + return sb.toString(); + } + + @Nullable + @Override + public List parse(@Nullable String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; + + int idx = ParseUtils.skipSpaces(value, 0); + if (value.charAt(idx++) != '[') + throw new IllegalArgumentException( + String.format( + "Cannot parse list value from \"%s\", at character %d expecting '[' but got '%c'", + value, idx, value.charAt(idx))); + + idx = ParseUtils.skipSpaces(value, idx); + + if (value.charAt(idx) == ']') { + return new ArrayList<>(0); + } + + List list = new ArrayList<>(); + while (idx < value.length()) { + int n; + try { + n = ParseUtils.skipCQLValue(value, idx); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format( + "Cannot parse list value from \"%s\", invalid CQL value at character %d", + value, idx), + e); + } + + list.add(elementCodec.parse(value.substring(idx, n))); + idx = n; + + idx = ParseUtils.skipSpaces(value, idx); + if (value.charAt(idx) == ']') return list; + if (value.charAt(idx++) != ',') + throw new IllegalArgumentException( + String.format( + "Cannot parse list value from \"%s\", at character %d expecting ',' but got '%c'", + value, idx, value.charAt(idx))); + + idx = ParseUtils.skipSpaces(value, idx); + } + throw new IllegalArgumentException( + String.format("Malformed list value \"%s\", missing closing ']'", value)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/MapCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/MapCodec.java new file mode 100644 index 00000000000..4f330b3ab59 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/MapCodec.java @@ -0,0 +1,255 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.collect.Maps; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.LinkedHashMap; +import java.util.Map; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class MapCodec implements TypeCodec> { + + private final DataType cqlType; + private final GenericType> javaType; + private final TypeCodec keyCodec; + private final TypeCodec valueCodec; + + public MapCodec(DataType cqlType, TypeCodec keyCodec, TypeCodec valueCodec) { + this.cqlType = cqlType; + this.keyCodec = keyCodec; + this.valueCodec = valueCodec; + this.javaType = GenericType.mapOf(keyCodec.getJavaType(), valueCodec.getJavaType()); + } + + @NonNull + @Override + public GenericType> getJavaType() { + return javaType; + } + + @NonNull + @Override + public DataType getCqlType() { + return cqlType; + } + + @Override + public boolean accepts(@NonNull Object value) { + if (value instanceof Map) { + // runtime type ok, now check key and value types + Map map = (Map) value; + if (map.isEmpty()) { + return true; + } + Map.Entry entry = map.entrySet().iterator().next(); + return keyCodec.accepts(entry.getKey()) && valueCodec.accepts(entry.getValue()); + } + return false; + } + + @Override + @Nullable + public ByteBuffer encode( + @Nullable Map value, @NonNull ProtocolVersion protocolVersion) { + // An int indicating the number of key/value pairs in the map, followed by the pairs. Each pair + // is a byte array representing the serialized key, preceded by an int indicating its size, + // followed by the value in the same format. + if (value == null) { + return null; + } else { + int i = 0; + ByteBuffer[] encodedElements = new ByteBuffer[value.size() * 2]; + int toAllocate = 4; // initialize with number of elements + for (Map.Entry entry : value.entrySet()) { + if (entry.getKey() == null) { + throw new NullPointerException("Map keys cannot be null"); + } + if (entry.getValue() == null) { + throw new NullPointerException("Map values cannot be null"); + } + ByteBuffer encodedKey; + try { + encodedKey = keyCodec.encode(entry.getKey(), protocolVersion); + } catch (ClassCastException e) { + throw new IllegalArgumentException("Invalid type for key: " + entry.getKey().getClass()); + } + if (encodedKey == null) { + throw new NullPointerException("Map keys cannot encode to CQL NULL"); + } + encodedElements[i++] = encodedKey; + toAllocate += 4 + encodedKey.remaining(); // the key preceded by its size + ByteBuffer encodedValue; + try { + encodedValue = valueCodec.encode(entry.getValue(), protocolVersion); + } catch (ClassCastException e) { + throw new IllegalArgumentException( + "Invalid type for value: " + entry.getValue().getClass()); + } + if (encodedValue == null) { + throw new NullPointerException("Map values cannot encode to CQL NULL"); + } + encodedElements[i++] = encodedValue; + toAllocate += 4 + encodedValue.remaining(); // the value preceded by its size + } + ByteBuffer result = ByteBuffer.allocate(toAllocate); + result.putInt(value.size()); + for (ByteBuffer encodedElement : encodedElements) { + result.putInt(encodedElement.remaining()); + result.put(encodedElement); + } + result.flip(); + return result; + } + } + + @Nullable + @Override + public Map decode( + @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return new LinkedHashMap<>(0); + } else { + ByteBuffer input = bytes.duplicate(); + int size = input.getInt(); + Map result = Maps.newLinkedHashMapWithExpectedSize(size); + for (int i = 0; i < size; i++) { + int keySize = input.getInt(); + ByteBuffer encodedKey = input.slice(); + encodedKey.limit(keySize); + input.position(input.position() + keySize); + KeyT key = keyCodec.decode(encodedKey, protocolVersion); + + int valueSize = input.getInt(); + ByteBuffer encodedValue = input.slice(); + encodedValue.limit(valueSize); + input.position(input.position() + valueSize); + ValueT value = valueCodec.decode(encodedValue, protocolVersion); + + result.put(key, value); + } + return result; + } + } + + @NonNull + @Override + public String format(@Nullable Map value) { + if (value == null) { + return "NULL"; + } + StringBuilder sb = new StringBuilder(); + sb.append("{"); + boolean first = true; + for (Map.Entry e : value.entrySet()) { + if (first) { + first = false; + } else { + sb.append(","); + } + sb.append(keyCodec.format(e.getKey())); + sb.append(":"); + sb.append(valueCodec.format(e.getValue())); + } + sb.append("}"); + return sb.toString(); + } + + @Nullable + @Override + public Map parse(@Nullable String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) { + return null; + } + + int idx = ParseUtils.skipSpaces(value, 0); + if (value.charAt(idx++) != '{') { + throw new IllegalArgumentException( + String.format( + "cannot parse map value from \"%s\", at character %d expecting '{' but got '%c'", + value, idx, value.charAt(idx))); + } + + idx = ParseUtils.skipSpaces(value, idx); + + if (value.charAt(idx) == '}') { + return new LinkedHashMap<>(0); + } + + Map map = new LinkedHashMap<>(); + while (idx < value.length()) { + int n; + try { + n = ParseUtils.skipCQLValue(value, idx); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format( + "Cannot parse map value from \"%s\", invalid CQL value at character %d", + value, idx), + e); + } + + KeyT k = keyCodec.parse(value.substring(idx, n)); + idx = n; + + idx = ParseUtils.skipSpaces(value, idx); + if (value.charAt(idx++) != ':') { + throw new IllegalArgumentException( + String.format( + "Cannot parse map value from \"%s\", at character %d expecting ':' but got '%c'", + value, idx, value.charAt(idx))); + } + idx = ParseUtils.skipSpaces(value, idx); + + try { + n = ParseUtils.skipCQLValue(value, idx); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format( + "Cannot parse map value from \"%s\", invalid CQL value at character %d", + value, idx), + e); + } + + ValueT v = valueCodec.parse(value.substring(idx, n)); + idx = n; + + map.put(k, v); + + idx = ParseUtils.skipSpaces(value, idx); + if (value.charAt(idx) == '}') { + return map; + } + if (value.charAt(idx++) != ',') { + throw new IllegalArgumentException( + String.format( + "Cannot parse map value from \"%s\", at character %d expecting ',' but got '%c'", + value, idx, value.charAt(idx))); + } + + idx = ParseUtils.skipSpaces(value, idx); + } + throw new IllegalArgumentException( + String.format("Malformed map value \"%s\", missing closing '}'", value)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/ParseUtils.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/ParseUtils.java new file mode 100644 index 00000000000..c8aa5e7df9b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/ParseUtils.java @@ -0,0 +1,141 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +public class ParseUtils { + + /** + * Returns the index of the first character in toParse from idx that is not a "space". + * + * @param toParse the string to skip space on. + * @param idx the index to start skipping space from. + * @return the index of the first character in toParse from idx that is not a "space. + */ + public static int skipSpaces(String toParse, int idx) { + while (isBlank(toParse.charAt(idx)) && idx < toParse.length()) ++idx; + return idx; + } + + /** + * Assuming that idx points to the beginning of a CQL value in toParse, returns the index of the + * first character after this value. + * + * @param toParse the string to skip a value form. + * @param idx the index to start parsing a value from. + * @return the index ending the CQL value starting at {@code idx}. + * @throws IllegalArgumentException if idx doesn't point to the start of a valid CQL value. + */ + public static int skipCQLValue(String toParse, int idx) { + if (idx >= toParse.length()) throw new IllegalArgumentException(); + + if (isBlank(toParse.charAt(idx))) throw new IllegalArgumentException(); + + int cbrackets = 0; + int sbrackets = 0; + int parens = 0; + boolean inString = false; + + do { + char c = toParse.charAt(idx); + if (inString) { + if (c == '\'') { + if (idx + 1 < toParse.length() && toParse.charAt(idx + 1) == '\'') { + ++idx; // this is an escaped quote, skip it + } else { + inString = false; + if (cbrackets == 0 && sbrackets == 0 && parens == 0) return idx + 1; + } + } + // Skip any other character + } else if (c == '\'') { + inString = true; + } else if (c == '{') { + ++cbrackets; + } else if (c == '[') { + ++sbrackets; + } else if (c == '(') { + ++parens; + } else if (c == '}') { + if (cbrackets == 0) return idx; + + --cbrackets; + if (cbrackets == 0 && sbrackets == 0 && parens == 0) return idx + 1; + } else if (c == ']') { + if (sbrackets == 0) return idx; + + --sbrackets; + if (cbrackets == 0 && sbrackets == 0 && parens == 0) return idx + 1; + } else if (c == ')') { + if (parens == 0) return idx; + + --parens; + if (cbrackets == 0 && sbrackets == 0 && parens == 0) return idx + 1; + } else if (isBlank(c) || !isCqlIdentifierChar(c)) { + if (cbrackets == 0 && sbrackets == 0 && parens == 0) return idx; + } + } while (++idx < toParse.length()); + + if (inString || cbrackets != 0 || sbrackets != 0 || parens != 0) + throw new IllegalArgumentException(); + return idx; + } + + /** + * Assuming that idx points to the beginning of a CQL identifier in toParse, returns the index of + * the first character after this identifier. + * + * @param toParse the string to skip an identifier from. + * @param idx the index to start parsing an identifier from. + * @return the index ending the CQL identifier starting at {@code idx}. + * @throws IllegalArgumentException if idx doesn't point to the start of a valid CQL identifier. + */ + public static int skipCQLId(String toParse, int idx) { + if (idx >= toParse.length()) throw new IllegalArgumentException(); + + char c = toParse.charAt(idx); + if (isCqlIdentifierChar(c)) { + while (idx < toParse.length() && isCqlIdentifierChar(toParse.charAt(idx))) idx++; + return idx; + } + + if (c != '"') throw new IllegalArgumentException(); + + while (++idx < toParse.length()) { + c = toParse.charAt(idx); + if (c != '"') continue; + + if (idx + 1 < toParse.length() && toParse.charAt(idx + 1) == '\"') + ++idx; // this is an escaped double quote, skip it + else return idx + 1; + } + throw new IllegalArgumentException(); + } + + public static boolean isBlank(int c) { + return c == ' ' || c == '\t' || c == '\n'; + } + + public static boolean isCqlIdentifierChar(int c) { + return (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || c == '-' + || c == '+' + || c == '.' + || c == '_' + || c == '&'; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/SetCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/SetCodec.java new file mode 100644 index 00000000000..7dc0c930c6e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/SetCodec.java @@ -0,0 +1,196 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.Sets; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.LinkedHashSet; +import java.util.Set; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class SetCodec implements TypeCodec> { + + private final DataType cqlType; + private final GenericType> javaType; + private final TypeCodec elementCodec; + + public SetCodec(DataType cqlType, TypeCodec elementCodec) { + this.cqlType = cqlType; + this.javaType = GenericType.setOf(elementCodec.getJavaType()); + this.elementCodec = elementCodec; + Preconditions.checkArgument(cqlType instanceof SetType); + } + + @NonNull + @Override + public GenericType> getJavaType() { + return javaType; + } + + @NonNull + @Override + public DataType getCqlType() { + return cqlType; + } + + @Override + public boolean accepts(@NonNull Object value) { + if (Set.class.isAssignableFrom(value.getClass())) { + // runtime type ok, now check element type + Set set = (Set) value; + return set.isEmpty() || elementCodec.accepts(set.iterator().next()); + } else { + return false; + } + } + + @Nullable + @Override + public ByteBuffer encode( + @Nullable Set value, @NonNull ProtocolVersion protocolVersion) { + // An int indicating the number of elements in the set, followed by the elements. Each element + // is a byte array representing the serialized value, preceded by an int indicating its size. + if (value == null) { + return null; + } else { + int i = 0; + ByteBuffer[] encodedElements = new ByteBuffer[value.size()]; + int toAllocate = 4; // initialize with number of elements + for (ElementT element : value) { + if (element == null) { + throw new NullPointerException("Collection elements cannot be null"); + } + ByteBuffer encodedElement; + try { + encodedElement = elementCodec.encode(element, protocolVersion); + } catch (ClassCastException e) { + throw new IllegalArgumentException("Invalid type for element: " + element.getClass()); + } + if (encodedElement == null) { + throw new NullPointerException("Collection elements cannot encode to CQL NULL"); + } + encodedElements[i++] = encodedElement; + toAllocate += 4 + encodedElement.remaining(); // the element preceded by its size + } + ByteBuffer result = ByteBuffer.allocate(toAllocate); + result.putInt(value.size()); + for (ByteBuffer encodedElement : encodedElements) { + result.putInt(encodedElement.remaining()); + result.put(encodedElement); + } + result.flip(); + return result; + } + } + + @Nullable + @Override + public Set decode( + @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return new LinkedHashSet<>(0); + } else { + ByteBuffer input = bytes.duplicate(); + int size = input.getInt(); + Set result = Sets.newLinkedHashSetWithExpectedSize(size); + for (int i = 0; i < size; i++) { + int elementSize = input.getInt(); + ByteBuffer encodedElement = input.slice(); + encodedElement.limit(elementSize); + input.position(input.position() + elementSize); + result.add(elementCodec.decode(encodedElement, protocolVersion)); + } + return result; + } + } + + @NonNull + @Override + public String format(@Nullable Set value) { + if (value == null) { + return "NULL"; + } + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (ElementT t : value) { + if (first) { + first = false; + } else { + sb.append(","); + } + sb.append(elementCodec.format(t)); + } + sb.append("}"); + return sb.toString(); + } + + @Nullable + @Override + public Set parse(@Nullable String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; + + int idx = ParseUtils.skipSpaces(value, 0); + if (value.charAt(idx++) != '{') + throw new IllegalArgumentException( + String.format( + "Cannot parse set value from \"%s\", at character %d expecting '{' but got '%c'", + value, idx, value.charAt(idx))); + + idx = ParseUtils.skipSpaces(value, idx); + + if (value.charAt(idx) == '}') { + return new LinkedHashSet<>(0); + } + + Set set = new LinkedHashSet<>(); + while (idx < value.length()) { + int n; + try { + n = ParseUtils.skipCQLValue(value, idx); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format( + "Cannot parse set value from \"%s\", invalid CQL value at character %d", + value, idx), + e); + } + + set.add(elementCodec.parse(value.substring(idx, n))); + idx = n; + + idx = ParseUtils.skipSpaces(value, idx); + if (value.charAt(idx) == '}') return set; + if (value.charAt(idx++) != ',') + throw new IllegalArgumentException( + String.format( + "Cannot parse set value from \"%s\", at character %d expecting ',' but got '%c'", + value, idx, value.charAt(idx))); + + idx = ParseUtils.skipSpaces(value, idx); + } + throw new IllegalArgumentException( + String.format("Malformed set value \"%s\", missing closing '}'", value)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/SmallIntCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/SmallIntCodec.java new file mode 100644 index 00000000000..d8ec3c2d414 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/SmallIntCodec.java @@ -0,0 +1,91 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveShortCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class SmallIntCodec implements PrimitiveShortCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.SHORT; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.SMALLINT; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof Short; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == Short.class || javaClass == short.class; + } + + @Nullable + @Override + public ByteBuffer encodePrimitive(short value, @NonNull ProtocolVersion protocolVersion) { + ByteBuffer bytes = ByteBuffer.allocate(2); + bytes.putShort(0, value); + return bytes; + } + + @Override + public short decodePrimitive( + @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return 0; + } else if (bytes.remaining() != 2) { + throw new IllegalArgumentException( + "Invalid 16-bits integer value, expecting 2 bytes but got " + bytes.remaining()); + } else { + return bytes.getShort(bytes.position()); + } + } + + @NonNull + @Override + public String format(@Nullable Short value) { + return (value == null) ? "NULL" : Short.toString(value); + } + + @Nullable + @Override + public Short parse(@Nullable String value) { + try { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : Short.parseShort(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse 16-bits int value from \"%s\"", value)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/StringCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/StringCodec.java new file mode 100644 index 00000000000..bffe3a10fd1 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/StringCodec.java @@ -0,0 +1,99 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.util.Strings; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class StringCodec implements TypeCodec { + + private final DataType cqlType; + private final Charset charset; + + public StringCodec(@NonNull DataType cqlType, @NonNull Charset charset) { + this.cqlType = cqlType; + this.charset = charset; + } + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.STRING; + } + + @NonNull + @Override + public DataType getCqlType() { + return cqlType; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof String; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == String.class; + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable String value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : ByteBuffer.wrap(value.getBytes(charset)); + } + + @Nullable + @Override + public String decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null) { + return null; + } else if (bytes.remaining() == 0) { + return ""; + } else { + return new String(Bytes.getArray(bytes), charset); + } + } + + @NonNull + @Override + public String format(@Nullable String value) { + return (value == null) ? "NULL" : Strings.quote(value); + } + + @Nullable + @Override + public String parse(String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) { + return null; + } else if (!Strings.isQuoted(value)) { + throw new IllegalArgumentException( + "text or varchar values must be enclosed by single quotes"); + } else { + return Strings.unquote(value); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TimeCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TimeCodec.java new file mode 100644 index 00000000000..5d862982acf --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TimeCodec.java @@ -0,0 +1,114 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.util.Strings; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class TimeCodec implements TypeCodec { + + private static final DateTimeFormatter FORMATTER = + DateTimeFormatter.ofPattern("HH:mm:ss.SSSSSSSSS"); + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.LOCAL_TIME; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.TIME; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof LocalTime; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == LocalTime.class; + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable LocalTime value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) + ? null + : TypeCodecs.BIGINT.encodePrimitive(value.toNanoOfDay(), protocolVersion); + } + + @Nullable + @Override + public LocalTime decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return null; + } else { + long nanosOfDay = TypeCodecs.BIGINT.decodePrimitive(bytes, protocolVersion); + return LocalTime.ofNanoOfDay(nanosOfDay); + } + } + + @NonNull + @Override + public String format(@Nullable LocalTime value) { + return (value == null) ? "NULL" : Strings.quote(FORMATTER.format(value)); + } + + @Nullable + @Override + public LocalTime parse(@Nullable String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) { + return null; + } + + // enclosing single quotes required, even for long literals + if (!Strings.isQuoted(value)) { + throw new IllegalArgumentException("time values must be enclosed by single quotes"); + } + value = value.substring(1, value.length() - 1); + + if (Strings.isLongLiteral(value)) { + try { + return LocalTime.ofNanoOfDay(Long.parseLong(value)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse time value from \"%s\"", value), e); + } + } + + try { + return LocalTime.parse(value); + } catch (RuntimeException e) { + throw new IllegalArgumentException( + String.format("Cannot parse time value from \"%s\"", value), e); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TimeUuidCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TimeUuidCodec.java new file mode 100644 index 00000000000..a8866fada21 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TimeUuidCodec.java @@ -0,0 +1,70 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.UUID; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class TimeUuidCodec extends UuidCodec { + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.TIMEUUID; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof UUID && ((UUID) value).version() == 1; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == UUID.class; + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable UUID value, @NonNull ProtocolVersion protocolVersion) { + if (value == null) { + return null; + } else if (value.version() != 1) { + throw new IllegalArgumentException( + String.format("%s is not a Type 1 (time-based) UUID", value)); + } else { + return super.encode(value, protocolVersion); + } + } + + @NonNull + @Override + public String format(@Nullable UUID value) { + if (value == null) { + return "NULL"; + } else if (value.version() != 1) { + throw new IllegalArgumentException( + String.format("%s is not a Type 1 (time-based) UUID", value)); + } else { + return super.format(value); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TimestampCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TimestampCodec.java new file mode 100644 index 00000000000..aa7d147581f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TimestampCodec.java @@ -0,0 +1,294 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.util.Strings; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import io.netty.util.concurrent.FastThreadLocal; +import java.nio.ByteBuffer; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Date; +import java.util.TimeZone; +import net.jcip.annotations.ThreadSafe; + +/** + * A codec that handles Apache Cassandra(R)'s timestamp type and maps it to Java's {@link Instant}. + * + *

Implementation notes: + * + *

    + *
  1. Because {@code Instant} uses a precision of nanoseconds, whereas the timestamp type uses a + * precision of milliseconds, truncation will happen for any excess precision information as + * though the amount in nanoseconds was subject to integer division by one million. + *
  2. For compatibility reasons, this codec uses the legacy {@link SimpleDateFormat} API + * internally when parsing and formatting, and converts from {@link Instant} to {@link Date} + * and vice versa. Specially when parsing, this may yield different results as compared to + * what the newer Java Time API parsers would have produced for the same input. + *
  3. Also, {@code Instant} can store points on the time-line further in the future and further + * in the past than {@code Date}. This codec will throw an exception when attempting to parse + * or format an {@code Instant} falling in this category. + *
+ * + *

Accepted date-time formats

+ * + * The following patterns are valid CQL timestamp literal formats for Apache Cassandra(R) 3.0 and + * higher, and are thus all recognized when parsing: + * + *
    + *
  1. {@code yyyy-MM-dd'T'HH:mm} + *
  2. {@code yyyy-MM-dd'T'HH:mm:ss} + *
  3. {@code yyyy-MM-dd'T'HH:mm:ss.SSS} + *
  4. {@code yyyy-MM-dd'T'HH:mmX} + *
  5. {@code yyyy-MM-dd'T'HH:mmXX} + *
  6. {@code yyyy-MM-dd'T'HH:mmXXX} + *
  7. {@code yyyy-MM-dd'T'HH:mm:ssX} + *
  8. {@code yyyy-MM-dd'T'HH:mm:ssXX} + *
  9. {@code yyyy-MM-dd'T'HH:mm:ssXXX} + *
  10. {@code yyyy-MM-dd'T'HH:mm:ss.SSSX} + *
  11. {@code yyyy-MM-dd'T'HH:mm:ss.SSSXX} + *
  12. {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX} + *
  13. {@code yyyy-MM-dd'T'HH:mm z} + *
  14. {@code yyyy-MM-dd'T'HH:mm:ss z} + *
  15. {@code yyyy-MM-dd'T'HH:mm:ss.SSS z} + *
  16. {@code yyyy-MM-dd HH:mm} + *
  17. {@code yyyy-MM-dd HH:mm:ss} + *
  18. {@code yyyy-MM-dd HH:mm:ss.SSS} + *
  19. {@code yyyy-MM-dd HH:mmX} + *
  20. {@code yyyy-MM-dd HH:mmXX} + *
  21. {@code yyyy-MM-dd HH:mmXXX} + *
  22. {@code yyyy-MM-dd HH:mm:ssX} + *
  23. {@code yyyy-MM-dd HH:mm:ssXX} + *
  24. {@code yyyy-MM-dd HH:mm:ssXXX} + *
  25. {@code yyyy-MM-dd HH:mm:ss.SSSX} + *
  26. {@code yyyy-MM-dd HH:mm:ss.SSSXX} + *
  27. {@code yyyy-MM-dd HH:mm:ss.SSSXXX} + *
  28. {@code yyyy-MM-dd HH:mm z} + *
  29. {@code yyyy-MM-dd HH:mm:ss z} + *
  30. {@code yyyy-MM-dd HH:mm:ss.SSS z} + *
  31. {@code yyyy-MM-dd} + *
  32. {@code yyyy-MM-ddX} + *
  33. {@code yyyy-MM-ddXX} + *
  34. {@code yyyy-MM-ddXXX} + *
  35. {@code yyyy-MM-dd z} + *
+ * + * By default, when parsing, timestamp literals that do not include any time zone information will + * be interpreted using the system's {@linkplain TimeZone#getDefault() default time zone}. This is + * intended to mimic Apache Cassandra(R)'s own parsing behavior (see {@code + * org.apache.cassandra.serializers.TimestampSerializer}). The default time zone can be modified + * using the {@linkplain TimestampCodec#TimestampCodec(ZoneId) one-arg constructor} that takes a + * custom {@link ZoneId} as an argument. + * + *

When formatting, the pattern used is always {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX} and the time + * zone is either the the system's default one, or the one that was provided when instantiating the + * codec. + */ +@ThreadSafe +public class TimestampCodec implements TypeCodec { + + /** + * Patterns accepted by Apache Cassandra(R) 3.0 and higher when parsing CQL literals. + * + *

Note that Cassandra's TimestampSerializer declares many more patterns but some of them are + * equivalent when parsing. + */ + private static final String[] DATE_STRING_PATTERNS = + new String[] { + // 1) date-time patterns separated by 'T' + // (declared first because none of the others are ISO compliant, but some of these are) + // 1.a) without time zone + "yyyy-MM-dd'T'HH:mm", + "yyyy-MM-dd'T'HH:mm:ss", + "yyyy-MM-dd'T'HH:mm:ss.SSS", + // 1.b) with ISO-8601 time zone + "yyyy-MM-dd'T'HH:mmX", + "yyyy-MM-dd'T'HH:mmXX", + "yyyy-MM-dd'T'HH:mmXXX", + "yyyy-MM-dd'T'HH:mm:ssX", + "yyyy-MM-dd'T'HH:mm:ssXX", + "yyyy-MM-dd'T'HH:mm:ssXXX", + "yyyy-MM-dd'T'HH:mm:ss.SSSX", + "yyyy-MM-dd'T'HH:mm:ss.SSSXX", + "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", + // 1.c) with generic time zone + "yyyy-MM-dd'T'HH:mm z", + "yyyy-MM-dd'T'HH:mm:ss z", + "yyyy-MM-dd'T'HH:mm:ss.SSS z", + // 2) date-time patterns separated by whitespace + // 2.a) without time zone + "yyyy-MM-dd HH:mm", + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm:ss.SSS", + // 2.b) with ISO-8601 time zone + "yyyy-MM-dd HH:mmX", + "yyyy-MM-dd HH:mmXX", + "yyyy-MM-dd HH:mmXXX", + "yyyy-MM-dd HH:mm:ssX", + "yyyy-MM-dd HH:mm:ssXX", + "yyyy-MM-dd HH:mm:ssXXX", + "yyyy-MM-dd HH:mm:ss.SSSX", + "yyyy-MM-dd HH:mm:ss.SSSXX", + "yyyy-MM-dd HH:mm:ss.SSSXXX", + // 2.c) with generic time zone + "yyyy-MM-dd HH:mm z", + "yyyy-MM-dd HH:mm:ss z", + "yyyy-MM-dd HH:mm:ss.SSS z", + // 3) date patterns without time + // 3.a) without time zone + "yyyy-MM-dd", + // 3.b) with ISO-8601 time zone + "yyyy-MM-ddX", + "yyyy-MM-ddXX", + "yyyy-MM-ddXXX", + // 3.c) with generic time zone + "yyyy-MM-dd z" + }; + + private final FastThreadLocal parser; + + private final FastThreadLocal formatter; + + /** + * Creates a new {@code TimestampCodec} that uses the system's {@linkplain ZoneId#systemDefault() + * default time zone} to parse timestamp literals that do not include any time zone information. + */ + public TimestampCodec() { + this(ZoneId.systemDefault()); + } + + /** + * Creates a new {@code TimestampCodec} that uses the given {@link ZoneId} to parse timestamp + * literals that do not include any time zone information. + */ + public TimestampCodec(ZoneId defaultZoneId) { + parser = + new FastThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + SimpleDateFormat parser = new SimpleDateFormat(); + parser.setLenient(false); + parser.setTimeZone(TimeZone.getTimeZone(defaultZoneId)); + return parser; + } + }; + formatter = + new FastThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); + parser.setTimeZone(TimeZone.getTimeZone(defaultZoneId)); + return parser; + } + }; + } + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.INSTANT; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.TIMESTAMP; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof Instant; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == Instant.class; + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable Instant value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) + ? null + : TypeCodecs.BIGINT.encodePrimitive(value.toEpochMilli(), protocolVersion); + } + + @Nullable + @Override + public Instant decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null || bytes.remaining() == 0) + ? null + : Instant.ofEpochMilli(TypeCodecs.BIGINT.decodePrimitive(bytes, protocolVersion)); + } + + @NonNull + @Override + public String format(@Nullable Instant value) { + return (value == null) ? "NULL" : Strings.quote(formatter.get().format(Date.from(value))); + } + + @Nullable + @Override + public Instant parse(@Nullable String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) { + return null; + } + String unquoted = Strings.unquote(value); + if (Strings.isLongLiteral(unquoted)) { + // Numeric literals may be quoted or not + try { + return Instant.ofEpochMilli(Long.parseLong(unquoted)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse timestamp value from \"%s\"", value)); + } + } else { + // Alphanumeric literals must be quoted + if (!Strings.isQuoted(value)) { + throw new IllegalArgumentException( + String.format("Alphanumeric timestamp literal must be quoted: \"%s\"", value)); + } + SimpleDateFormat parser = this.parser.get(); + TimeZone timeZone = parser.getTimeZone(); + ParsePosition pos = new ParsePosition(0); + for (String pattern : DATE_STRING_PATTERNS) { + parser.applyPattern(pattern); + pos.setIndex(0); + try { + Date date = parser.parse(unquoted, pos); + if (date != null && pos.getIndex() == unquoted.length()) { + return date.toInstant(); + } + } finally { + // restore the parser's default time zone, it might have been modified by the call to + // parse() + parser.setTimeZone(timeZone); + } + } + throw new IllegalArgumentException( + String.format("Cannot parse timestamp value from \"%s\"", value)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TinyIntCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TinyIntCodec.java new file mode 100644 index 00000000000..27241aa7833 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TinyIntCodec.java @@ -0,0 +1,91 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveByteCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class TinyIntCodec implements PrimitiveByteCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.BYTE; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.TINYINT; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof Byte; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == Byte.class || javaClass == byte.class; + } + + @Nullable + @Override + public ByteBuffer encodePrimitive(byte value, @NonNull ProtocolVersion protocolVersion) { + ByteBuffer bytes = ByteBuffer.allocate(1); + bytes.put(0, value); + return bytes; + } + + @Override + public byte decodePrimitive( + @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return 0; + } else if (bytes.remaining() != 1) { + throw new IllegalArgumentException( + "Invalid 8-bits integer value, expecting 1 byte but got " + bytes.remaining()); + } else { + return bytes.get(bytes.position()); + } + } + + @NonNull + @Override + public String format(@Nullable Byte value) { + return (value == null) ? "NULL" : Byte.toString(value); + } + + @Nullable + @Override + public Byte parse(@Nullable String value) { + try { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : Byte.parseByte(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse 8-bits int value from \"%s\"", value)); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TupleCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TupleCodec.java new file mode 100644 index 00000000000..2a900ce7a10 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/TupleCodec.java @@ -0,0 +1,219 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class TupleCodec implements TypeCodec { + + private final TupleType cqlType; + + public TupleCodec(@NonNull TupleType cqlType) { + this.cqlType = cqlType; + } + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.TUPLE_VALUE; + } + + @NonNull + @Override + public DataType getCqlType() { + return cqlType; + } + + @Override + public boolean accepts(@NonNull Object value) { + return (value instanceof TupleValue) && ((TupleValue) value).getType().equals(cqlType); + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return TupleValue.class.equals(javaClass); + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable TupleValue value, @NonNull ProtocolVersion protocolVersion) { + if (value == null) { + return null; + } + if (!value.getType().equals(cqlType)) { + throw new IllegalArgumentException( + String.format("Invalid tuple type, expected %s but got %s", cqlType, value.getType())); + } + // Encoding: each field as a [bytes] value ([bytes] = int length + contents, null is + // represented by -1) + int toAllocate = 0; + for (int i = 0; i < value.size(); i++) { + ByteBuffer field = value.getBytesUnsafe(i); + toAllocate += 4 + (field == null ? 0 : field.remaining()); + } + ByteBuffer result = ByteBuffer.allocate(toAllocate); + for (int i = 0; i < value.size(); i++) { + ByteBuffer field = value.getBytesUnsafe(i); + if (field == null) { + result.putInt(-1); + } else { + result.putInt(field.remaining()); + result.put(field.duplicate()); + } + } + return (ByteBuffer) result.flip(); + } + + @Nullable + @Override + public TupleValue decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null) { + return null; + } + // empty byte buffers will result in empty values + try { + ByteBuffer input = bytes.duplicate(); + TupleValue value = cqlType.newValue(); + int i = 0; + while (input.hasRemaining()) { + if (i > cqlType.getComponentTypes().size()) { + throw new IllegalArgumentException( + String.format( + "Too many fields in encoded tuple, expected %d", + cqlType.getComponentTypes().size())); + } + int elementSize = input.getInt(); + ByteBuffer element; + if (elementSize == -1) { + element = null; + } else { + element = input.slice(); + element.limit(elementSize); + input.position(input.position() + elementSize); + } + value = value.setBytesUnsafe(i, element); + i += 1; + } + return value; + } catch (BufferUnderflowException e) { + throw new IllegalArgumentException("Not enough bytes to deserialize a tuple", e); + } + } + + @NonNull + @Override + public String format(@Nullable TupleValue value) { + if (value == null) { + return "NULL"; + } + if (!value.getType().equals(cqlType)) { + throw new IllegalArgumentException( + String.format("Invalid tuple type, expected %s but got %s", cqlType, value.getType())); + } + CodecRegistry registry = cqlType.getAttachmentPoint().getCodecRegistry(); + + StringBuilder sb = new StringBuilder("("); + boolean first = true; + for (int i = 0; i < value.size(); i++) { + if (first) { + first = false; + } else { + sb.append(","); + } + DataType elementType = cqlType.getComponentTypes().get(i); + TypeCodec codec = registry.codecFor(elementType); + sb.append(codec.format(value.get(i, codec))); + } + sb.append(")"); + return sb.toString(); + } + + @Nullable + @Override + public TupleValue parse(@Nullable String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) { + return null; + } + + TupleValue tuple = cqlType.newValue(); + + int position = ParseUtils.skipSpaces(value, 0); + if (value.charAt(position++) != '(') { + throw new IllegalArgumentException( + String.format( + "Cannot parse tuple value from \"%s\", at character %d expecting '(' but got '%c'", + value, position, value.charAt(position))); + } + + position = ParseUtils.skipSpaces(value, position); + + if (value.charAt(position) == ')') { + return tuple; + } + + CodecRegistry registry = cqlType.getAttachmentPoint().getCodecRegistry(); + + int i = 0; + while (position < value.length()) { + int n; + try { + n = ParseUtils.skipCQLValue(value, position); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format( + "Cannot parse tuple value from \"%s\", invalid CQL value at character %d", + value, position), + e); + } + + String fieldValue = value.substring(position, n); + DataType elementType = cqlType.getComponentTypes().get(i); + TypeCodec codec = registry.codecFor(elementType); + tuple = tuple.set(i, codec.parse(fieldValue), codec); + + position = n; + i += 1; + + position = ParseUtils.skipSpaces(value, position); + if (value.charAt(position) == ')') { + return tuple; + } + if (value.charAt(position) != ',') { + throw new IllegalArgumentException( + String.format( + "Cannot parse tuple value from \"%s\", at character %d expecting ',' but got '%c'", + value, position, value.charAt(position))); + } + ++position; // skip ',' + + position = ParseUtils.skipSpaces(value, position); + } + throw new IllegalArgumentException( + String.format("Malformed tuple value \"%s\", missing closing ')'", value)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/UdtCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/UdtCodec.java new file mode 100644 index 00000000000..2e2df95ad33 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/UdtCodec.java @@ -0,0 +1,247 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class UdtCodec implements TypeCodec { + + private final UserDefinedType cqlType; + + public UdtCodec(@NonNull UserDefinedType cqlType) { + this.cqlType = cqlType; + } + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.UDT_VALUE; + } + + @NonNull + @Override + public DataType getCqlType() { + return cqlType; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof UdtValue && ((UdtValue) value).getType().equals(cqlType); + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return UdtValue.class.equals(javaClass); + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable UdtValue value, @NonNull ProtocolVersion protocolVersion) { + if (value == null) { + return null; + } + if (!value.getType().equals(cqlType)) { + throw new IllegalArgumentException( + String.format( + "Invalid user defined type, expected %s but got %s", cqlType, value.getType())); + } + // Encoding: each field as a [bytes] value ([bytes] = int length + contents, null is + // represented by -1) + int toAllocate = 0; + int size = cqlType.getFieldTypes().size(); + for (int i = 0; i < size; i++) { + ByteBuffer field = value.getBytesUnsafe(i); + toAllocate += 4 + (field == null ? 0 : field.remaining()); + } + ByteBuffer result = ByteBuffer.allocate(toAllocate); + for (int i = 0; i < value.size(); i++) { + ByteBuffer field = value.getBytesUnsafe(i); + if (field == null) { + result.putInt(-1); + } else { + result.putInt(field.remaining()); + result.put(field.duplicate()); + } + } + return (ByteBuffer) result.flip(); + } + + @Nullable + @Override + public UdtValue decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null) { + return null; + } + // empty byte buffers will result in empty values + try { + ByteBuffer input = bytes.duplicate(); + UdtValue value = cqlType.newValue(); + int i = 0; + while (input.hasRemaining()) { + if (i > cqlType.getFieldTypes().size()) { + throw new IllegalArgumentException( + String.format( + "Too many fields in encoded UDT value, expected %d", + cqlType.getFieldTypes().size())); + } + int elementSize = input.getInt(); + ByteBuffer element; + if (elementSize == -1) { + element = null; + } else { + element = input.slice(); + element.limit(elementSize); + input.position(input.position() + elementSize); + } + value = value.setBytesUnsafe(i, element); + i += 1; + } + return value; + } catch (BufferUnderflowException e) { + throw new IllegalArgumentException("Not enough bytes to deserialize a UDT value", e); + } + } + + @NonNull + @Override + public String format(@Nullable UdtValue value) { + if (value == null) { + return "NULL"; + } + + CodecRegistry registry = cqlType.getAttachmentPoint().getCodecRegistry(); + + StringBuilder sb = new StringBuilder("{"); + int size = cqlType.getFieldTypes().size(); + boolean first = true; + for (int i = 0; i < size; i++) { + if (first) { + first = false; + } else { + sb.append(","); + } + CqlIdentifier elementName = cqlType.getFieldNames().get(i); + sb.append(elementName.asCql(true)); + sb.append(":"); + DataType elementType = cqlType.getFieldTypes().get(i); + TypeCodec codec = registry.codecFor(elementType); + sb.append(codec.format(value.get(i, codec))); + } + sb.append("}"); + return sb.toString(); + } + + @Nullable + @Override + public UdtValue parse(@Nullable String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) { + return null; + } + + UdtValue udt = cqlType.newValue(); + + int position = ParseUtils.skipSpaces(value, 0); + if (value.charAt(position++) != '{') { + throw new IllegalArgumentException( + String.format( + "Cannot parse UDT value from \"%s\", at character %d expecting '{' but got '%c'", + value, position, value.charAt(position))); + } + + position = ParseUtils.skipSpaces(value, position); + + if (value.charAt(position) == '}') { + return udt; + } + + CodecRegistry registry = cqlType.getAttachmentPoint().getCodecRegistry(); + + while (position < value.length()) { + int n; + try { + n = ParseUtils.skipCQLId(value, position); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format( + "Cannot parse UDT value from \"%s\", cannot parse a CQL identifier at character %d", + value, position), + e); + } + CqlIdentifier id = CqlIdentifier.fromInternal(value.substring(position, n)); + position = n; + + if (!cqlType.contains(id)) { + throw new IllegalArgumentException( + String.format("Unknown field %s in value \"%s\"", id, value)); + } + + position = ParseUtils.skipSpaces(value, position); + if (value.charAt(position++) != ':') { + throw new IllegalArgumentException( + String.format( + "Cannot parse UDT value from \"%s\", at character %d expecting ':' but got '%c'", + value, position, value.charAt(position))); + } + position = ParseUtils.skipSpaces(value, position); + + try { + n = ParseUtils.skipCQLValue(value, position); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format( + "Cannot parse UDT value from \"%s\", invalid CQL value at character %d", + value, position), + e); + } + + String fieldValue = value.substring(position, n); + // This works because ids occur at most once in UDTs + DataType fieldType = cqlType.getFieldTypes().get(cqlType.firstIndexOf(id)); + TypeCodec codec = registry.codecFor(fieldType); + udt = udt.set(id, codec.parse(fieldValue), codec); + position = n; + + position = ParseUtils.skipSpaces(value, position); + if (value.charAt(position) == '}') { + return udt; + } + if (value.charAt(position) != ',') { + throw new IllegalArgumentException( + String.format( + "Cannot parse UDT value from \"%s\", at character %d expecting ',' but got '%c'", + value, position, value.charAt(position))); + } + ++position; // skip ',' + + position = ParseUtils.skipSpaces(value, position); + } + throw new IllegalArgumentException( + String.format("Malformed UDT value \"%s\", missing closing '}'", value)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/UuidCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/UuidCodec.java new file mode 100644 index 00000000000..ba3ef0ab110 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/UuidCodec.java @@ -0,0 +1,96 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.util.UUID; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class UuidCodec implements TypeCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.UUID; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.UUID; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof UUID; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == UUID.class; + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable UUID value, @NonNull ProtocolVersion protocolVersion) { + if (value == null) { + return null; + } + ByteBuffer bytes = ByteBuffer.allocate(16); + bytes.putLong(0, value.getMostSignificantBits()); + bytes.putLong(8, value.getLeastSignificantBits()); + return bytes; + } + + @Nullable + @Override + public UUID decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + if (bytes == null || bytes.remaining() == 0) { + return null; + } else if (bytes.remaining() != 16) { + throw new IllegalArgumentException( + "Unexpected number of bytes for a UUID, expected 16, got " + bytes.remaining()); + } else { + return new UUID(bytes.getLong(bytes.position()), bytes.getLong(bytes.position() + 8)); + } + } + + @NonNull + @Override + public String format(@Nullable UUID value) { + return (value == null) ? "NULL" : value.toString(); + } + + @Nullable + @Override + public UUID parse(@Nullable String value) { + try { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : UUID.fromString(value); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format("Cannot parse UUID value from \"%s\"", value), e); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/VarIntCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/VarIntCodec.java new file mode 100644 index 00000000000..eec3ee239b3 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/VarIntCodec.java @@ -0,0 +1,84 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.protocol.internal.util.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import net.jcip.annotations.ThreadSafe; + +@ThreadSafe +public class VarIntCodec implements TypeCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.BIG_INTEGER; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.VARINT; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof BigInteger; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return BigInteger.class.isAssignableFrom(javaClass); + } + + @Nullable + @Override + public ByteBuffer encode(@Nullable BigInteger value, @NonNull ProtocolVersion protocolVersion) { + return (value == null) ? null : ByteBuffer.wrap(value.toByteArray()); + } + + @Nullable + @Override + public BigInteger decode(@Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return (bytes == null) || bytes.remaining() == 0 ? null : new BigInteger(Bytes.getArray(bytes)); + } + + @NonNull + @Override + public String format(@Nullable BigInteger value) { + return (value == null) ? "NULL" : value.toString(); + } + + @Nullable + @Override + public BigInteger parse(@Nullable String value) { + try { + return (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) + ? null + : new BigInteger(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("Cannot parse varint value from \"%s\"", value), e); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/ZonedTimestampCodec.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/ZonedTimestampCodec.java new file mode 100644 index 00000000000..16649fd8daa --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/ZonedTimestampCodec.java @@ -0,0 +1,126 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import net.jcip.annotations.ThreadSafe; + +/** + * A codec that handles Apache Cassandra(R)'s timestamp type and maps it to Java's {@link + * ZonedDateTime}, using the {@link ZoneId} supplied at instantiation. + * + *

Note that Apache Cassandra(R)'s timestamp type does not store any time zone; this codec is + * provided merely as a convenience for users that need to deal with zoned timestamps in their + * applications. + * + *

This codec shares its logic with {@link TimestampCodec}. See the javadocs of this codec for + * important remarks about implementation notes and accepted timestamp formats. + * + * @see TimestampCodec + */ +@ThreadSafe +public class ZonedTimestampCodec implements TypeCodec { + + private final TypeCodec instantCodec; + private final ZoneId timeZone; + + /** + * Creates a new {@code ZonedTimestampCodec} that converts CQL timestamps into {@link + * ZonedDateTime} instances using the system's {@linkplain ZoneId#systemDefault() default time + * zone} as their time zone. The supplied {@code timeZone} will also be used to parse CQL + * timestamp literals that do not include any time zone information. + */ + public ZonedTimestampCodec() { + this(ZoneId.systemDefault()); + } + + /** + * Creates a new {@code ZonedTimestampCodec} that converts CQL timestamps into {@link + * ZonedDateTime} instances using the given {@link ZoneId} as their time zone. The supplied {@code + * timeZone} will also be used to parse CQL timestamp literals that do not include any time zone + * information. + */ + public ZonedTimestampCodec(ZoneId timeZone) { + instantCodec = new TimestampCodec(timeZone); + this.timeZone = timeZone; + } + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.ZONED_DATE_TIME; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.TIMESTAMP; + } + + @Override + public boolean accepts(@NonNull Object value) { + return value instanceof ZonedDateTime; + } + + @Override + public boolean accepts(@NonNull Class javaClass) { + return javaClass == ZonedDateTime.class; + } + + @Nullable + @Override + public ByteBuffer encode( + @Nullable ZonedDateTime value, @NonNull ProtocolVersion protocolVersion) { + return instantCodec.encode(value != null ? value.toInstant() : null, protocolVersion); + } + + @Nullable + @Override + public ZonedDateTime decode( + @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + Instant instant = instantCodec.decode(bytes, protocolVersion); + if (instant == null) { + return null; + } + return instant.atZone(timeZone); + } + + @NonNull + @Override + public String format(@Nullable ZonedDateTime value) { + return instantCodec.format(value != null ? value.toInstant() : null); + } + + @Nullable + @Override + public ZonedDateTime parse(@Nullable String value) { + Instant instant = instantCodec.parse(value); + if (instant == null) { + return null; + } + return instant.atZone(timeZone); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/registry/CachingCodecRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/registry/CachingCodecRegistry.java new file mode 100644 index 00000000000..740591cf3a7 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/registry/CachingCodecRegistry.java @@ -0,0 +1,416 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec.registry; + +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.CustomType; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.MapType; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.reflect.TypeToken; +import com.datastax.oss.protocol.internal.util.IntMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Set; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A codec registry that handles built-in type mappings, can be extended with a list of + * user-provided codecs, generates more complex codecs from those basic codecs, and caches generated + * codecs for reuse. + * + *

The primitive mappings always take precedence over any user codec. The list of user codecs can + * not be modified after construction. + * + *

This class is abstract in order to be agnostic from the cache implementation. Subclasses must + * implement {@link #getCachedCodec(DataType, GenericType, boolean)}. + */ +@ThreadSafe +public abstract class CachingCodecRegistry implements CodecRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(CachingCodecRegistry.class); + + // Implementation notes: + // - built-in primitive codecs are served directly, without hitting the cache + // - same for user codecs (we assume the cardinality will always be low, so a sequential array + // traversal is cheap). + + protected final String logPrefix; + private final TypeCodec[] primitiveCodecs; + private final TypeCodec[] userCodecs; + private final IntMap primitiveCodecsByCode; + + protected CachingCodecRegistry( + String logPrefix, TypeCodec[] primitiveCodecs, TypeCodec[] userCodecs) { + this.logPrefix = logPrefix; + this.primitiveCodecs = primitiveCodecs; + this.userCodecs = userCodecs; + this.primitiveCodecsByCode = sortByProtocolCode(primitiveCodecs); + } + + /** + * Gets a complex codec from the cache. + * + *

If the codec does not exist in the cache, this method must generate it with {@link + * #createCodec(DataType, GenericType, boolean)} (and most likely put it in the cache too for + * future calls). + */ + protected abstract TypeCodec getCachedCodec( + DataType cqlType, GenericType javaType, boolean isJavaCovariant); + + @NonNull + @Override + public TypeCodec codecFor( + @NonNull DataType cqlType, @NonNull GenericType javaType) { + return codecFor(cqlType, javaType, false); + } + + // Not exposed publicly, (isJavaCovariant=true) is only used for internal recursion + protected TypeCodec codecFor( + DataType cqlType, GenericType javaType, boolean isJavaCovariant) { + LOG.trace("[{}] Looking up codec for {} <-> {}", logPrefix, cqlType, javaType); + TypeCodec primitiveCodec = primitiveCodecsByCode.get(cqlType.getProtocolCode()); + if (primitiveCodec != null && matches(primitiveCodec, javaType, isJavaCovariant)) { + LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec); + return uncheckedCast(primitiveCodec); + } + for (TypeCodec userCodec : userCodecs) { + if (userCodec.accepts(cqlType) && matches(userCodec, javaType, isJavaCovariant)) { + LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec); + return uncheckedCast(userCodec); + } + } + return uncheckedCast(getCachedCodec(cqlType, javaType, isJavaCovariant)); + } + + @NonNull + @Override + public TypeCodec codecFor( + @NonNull DataType cqlType, @NonNull Class javaType) { + LOG.trace("[{}] Looking up codec for {} <-> {}", logPrefix, cqlType, javaType); + TypeCodec primitiveCodec = primitiveCodecsByCode.get(cqlType.getProtocolCode()); + if (primitiveCodec != null && primitiveCodec.accepts(javaType)) { + LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec); + return uncheckedCast(primitiveCodec); + } + for (TypeCodec userCodec : userCodecs) { + if (userCodec.accepts(cqlType) && userCodec.accepts(javaType)) { + LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec); + return uncheckedCast(userCodec); + } + } + return uncheckedCast(getCachedCodec(cqlType, GenericType.of(javaType), false)); + } + + @NonNull + @Override + public TypeCodec codecFor(@NonNull DataType cqlType) { + LOG.trace("[{}] Looking up codec for CQL type {}", logPrefix, cqlType); + TypeCodec primitiveCodec = primitiveCodecsByCode.get(cqlType.getProtocolCode()); + if (primitiveCodec != null) { + LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec); + return uncheckedCast(primitiveCodec); + } + for (TypeCodec userCodec : userCodecs) { + if (userCodec.accepts(cqlType)) { + LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec); + return uncheckedCast(userCodec); + } + } + return uncheckedCast(getCachedCodec(cqlType, null, false)); + } + + @NonNull + @Override + public TypeCodec codecFor( + @NonNull DataType cqlType, @NonNull JavaTypeT value) { + Preconditions.checkNotNull(cqlType); + Preconditions.checkNotNull(value); + LOG.trace("[{}] Looking up codec for CQL type {} and object {}", logPrefix, cqlType, value); + + TypeCodec primitiveCodec = primitiveCodecsByCode.get(cqlType.getProtocolCode()); + if (primitiveCodec != null && primitiveCodec.accepts(value)) { + LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec); + return uncheckedCast(primitiveCodec); + } + for (TypeCodec userCodec : userCodecs) { + if (userCodec.accepts(cqlType) && userCodec.accepts(value)) { + LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec); + return uncheckedCast(userCodec); + } + } + + if (value instanceof TupleValue) { + return uncheckedCast(codecFor(cqlType, TupleValue.class)); + } else if (value instanceof UdtValue) { + return uncheckedCast(codecFor(cqlType, UdtValue.class)); + } + + GenericType javaType = inspectType(value); + LOG.trace("[{}] Continuing based on inferred type {}", logPrefix, javaType); + return uncheckedCast(getCachedCodec(cqlType, javaType, true)); + } + + @NonNull + @Override + public TypeCodec codecFor(@NonNull JavaTypeT value) { + Preconditions.checkNotNull(value); + LOG.trace("[{}] Looking up codec for object {}", logPrefix, value); + + for (TypeCodec primitiveCodec : primitiveCodecs) { + if (primitiveCodec.accepts(value)) { + LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec); + return uncheckedCast(primitiveCodec); + } + } + for (TypeCodec userCodec : userCodecs) { + if (userCodec.accepts(value)) { + LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec); + return uncheckedCast(userCodec); + } + } + + if (value instanceof TupleValue) { + return uncheckedCast(codecFor(((TupleValue) value).getType(), TupleValue.class)); + } else if (value instanceof UdtValue) { + return uncheckedCast(codecFor(((UdtValue) value).getType(), UdtValue.class)); + } + + GenericType javaType = inspectType(value); + LOG.trace("[{}] Continuing based on inferred type {}", logPrefix, javaType); + return uncheckedCast(getCachedCodec(null, javaType, true)); + } + + @NonNull + @Override + public TypeCodec codecFor(@NonNull GenericType javaType) { + return codecFor(javaType, false); + } + + // Not exposed publicly, (isJavaCovariant=true) is only used for internal recursion + protected TypeCodec codecFor( + GenericType javaType, boolean isJavaCovariant) { + LOG.trace( + "[{}] Looking up codec for Java type {} (covariant = {})", + logPrefix, + javaType, + isJavaCovariant); + for (TypeCodec primitiveCodec : primitiveCodecs) { + if (matches(primitiveCodec, javaType, isJavaCovariant)) { + LOG.trace("[{}] Found matching primitive codec {}", logPrefix, primitiveCodec); + return uncheckedCast(primitiveCodec); + } + } + for (TypeCodec userCodec : userCodecs) { + if (matches(userCodec, javaType, isJavaCovariant)) { + LOG.trace("[{}] Found matching user codec {}", logPrefix, userCodec); + return uncheckedCast(userCodec); + } + } + return uncheckedCast(getCachedCodec(null, javaType, isJavaCovariant)); + } + + protected boolean matches(TypeCodec codec, GenericType javaType, boolean isJavaCovariant) { + return (isJavaCovariant) + ? codec.getJavaType().isSupertypeOf(javaType) + : codec.accepts(javaType); + } + + protected GenericType inspectType(Object value) { + if (value instanceof List) { + List list = (List) value; + if (list.isEmpty()) { + // The empty list is always encoded the same way, so any element type will do + return GenericType.listOf(Boolean.class); + } else { + GenericType elementType = inspectType(list.get(0)); + return GenericType.listOf(elementType); + } + } else if (value instanceof Set) { + Set set = (Set) value; + if (set.isEmpty()) { + return GenericType.setOf(Boolean.class); + } else { + GenericType elementType = inspectType(set.iterator().next()); + return GenericType.setOf(elementType); + } + } else if (value instanceof Map) { + Map map = (Map) value; + if (map.isEmpty()) { + return GenericType.mapOf(Boolean.class, Boolean.class); + } else { + Map.Entry entry = map.entrySet().iterator().next(); + GenericType keyType = inspectType(entry.getKey()); + GenericType valueType = inspectType(entry.getValue()); + return GenericType.mapOf(keyType, valueType); + } + } else { + // There's not much more we can do + return GenericType.of(value.getClass()); + } + } + + // Try to create a codec when we haven't found it in the cache + protected TypeCodec createCodec( + DataType cqlType, GenericType javaType, boolean isJavaCovariant) { + LOG.trace("[{}] Cache miss, creating codec", logPrefix); + // Either type can be null, but not both. + if (javaType == null) { + assert cqlType != null; + return createCodec(cqlType); + } else if (cqlType == null) { + return createCodec(javaType, isJavaCovariant); + } else { // Both non-null + TypeToken token = javaType.__getToken(); + if (cqlType instanceof ListType && List.class.isAssignableFrom(token.getRawType())) { + DataType elementCqlType = ((ListType) cqlType).getElementType(); + TypeCodec elementCodec; + if (token.getType() instanceof ParameterizedType) { + Type[] typeArguments = ((ParameterizedType) token.getType()).getActualTypeArguments(); + GenericType elementJavaType = GenericType.of(typeArguments[0]); + elementCodec = uncheckedCast(codecFor(elementCqlType, elementJavaType, isJavaCovariant)); + } else { + elementCodec = codecFor(elementCqlType); + } + return TypeCodecs.listOf(elementCodec); + } else if (cqlType instanceof SetType && Set.class.isAssignableFrom(token.getRawType())) { + DataType elementCqlType = ((SetType) cqlType).getElementType(); + TypeCodec elementCodec; + if (token.getType() instanceof ParameterizedType) { + Type[] typeArguments = ((ParameterizedType) token.getType()).getActualTypeArguments(); + GenericType elementJavaType = GenericType.of(typeArguments[0]); + elementCodec = uncheckedCast(codecFor(elementCqlType, elementJavaType, isJavaCovariant)); + } else { + elementCodec = codecFor(elementCqlType); + } + return TypeCodecs.setOf(elementCodec); + } else if (cqlType instanceof MapType && Map.class.isAssignableFrom(token.getRawType())) { + DataType keyCqlType = ((MapType) cqlType).getKeyType(); + DataType valueCqlType = ((MapType) cqlType).getValueType(); + TypeCodec keyCodec; + TypeCodec valueCodec; + if (token.getType() instanceof ParameterizedType) { + Type[] typeArguments = ((ParameterizedType) token.getType()).getActualTypeArguments(); + GenericType keyJavaType = GenericType.of(typeArguments[0]); + GenericType valueJavaType = GenericType.of(typeArguments[1]); + keyCodec = uncheckedCast(codecFor(keyCqlType, keyJavaType, isJavaCovariant)); + valueCodec = uncheckedCast(codecFor(valueCqlType, valueJavaType, isJavaCovariant)); + } else { + keyCodec = codecFor(keyCqlType); + valueCodec = codecFor(valueCqlType); + } + return TypeCodecs.mapOf(keyCodec, valueCodec); + } else if (cqlType instanceof TupleType + && TupleValue.class.isAssignableFrom(token.getRawType())) { + return TypeCodecs.tupleOf((TupleType) cqlType); + } else if (cqlType instanceof UserDefinedType + && UdtValue.class.isAssignableFrom(token.getRawType())) { + return TypeCodecs.udtOf((UserDefinedType) cqlType); + } else if (cqlType instanceof CustomType + && ByteBuffer.class.isAssignableFrom(token.getRawType())) { + return TypeCodecs.custom(cqlType); + } + throw new CodecNotFoundException(cqlType, javaType); + } + } + + // Try to create a codec when we haven't found it in the cache. + // Variant where the CQL type is unknown. Can be covariant if we come from a lookup by Java value. + protected TypeCodec createCodec(GenericType javaType, boolean isJavaCovariant) { + TypeToken token = javaType.__getToken(); + if (List.class.isAssignableFrom(token.getRawType()) + && token.getType() instanceof ParameterizedType) { + Type[] typeArguments = ((ParameterizedType) token.getType()).getActualTypeArguments(); + GenericType elementType = GenericType.of(typeArguments[0]); + TypeCodec elementCodec = codecFor(elementType, isJavaCovariant); + return TypeCodecs.listOf(elementCodec); + } else if (Set.class.isAssignableFrom(token.getRawType()) + && token.getType() instanceof ParameterizedType) { + Type[] typeArguments = ((ParameterizedType) token.getType()).getActualTypeArguments(); + GenericType elementType = GenericType.of(typeArguments[0]); + TypeCodec elementCodec = codecFor(elementType, isJavaCovariant); + return TypeCodecs.setOf(elementCodec); + } else if (Map.class.isAssignableFrom(token.getRawType()) + && token.getType() instanceof ParameterizedType) { + Type[] typeArguments = ((ParameterizedType) token.getType()).getActualTypeArguments(); + GenericType keyType = GenericType.of(typeArguments[0]); + GenericType valueType = GenericType.of(typeArguments[1]); + TypeCodec keyCodec = codecFor(keyType, isJavaCovariant); + TypeCodec valueCodec = codecFor(valueType, isJavaCovariant); + return TypeCodecs.mapOf(keyCodec, valueCodec); + } + throw new CodecNotFoundException(null, javaType); + } + + // Try to create a codec when we haven't found it in the cache. + // Variant where the Java type is unknown. + protected TypeCodec createCodec(DataType cqlType) { + if (cqlType instanceof ListType) { + DataType elementType = ((ListType) cqlType).getElementType(); + TypeCodec elementCodec = codecFor(elementType); + return TypeCodecs.listOf(elementCodec); + } else if (cqlType instanceof SetType) { + DataType elementType = ((SetType) cqlType).getElementType(); + TypeCodec elementCodec = codecFor(elementType); + return TypeCodecs.setOf(elementCodec); + } else if (cqlType instanceof MapType) { + DataType keyType = ((MapType) cqlType).getKeyType(); + DataType valueType = ((MapType) cqlType).getValueType(); + TypeCodec keyCodec = codecFor(keyType); + TypeCodec valueCodec = codecFor(valueType); + return TypeCodecs.mapOf(keyCodec, valueCodec); + } else if (cqlType instanceof TupleType) { + return TypeCodecs.tupleOf((TupleType) cqlType); + } else if (cqlType instanceof UserDefinedType) { + return TypeCodecs.udtOf((UserDefinedType) cqlType); + } else if (cqlType instanceof CustomType) { + return TypeCodecs.custom(cqlType); + } + throw new CodecNotFoundException(cqlType, null); + } + + private static IntMap sortByProtocolCode(TypeCodec[] codecs) { + IntMap.Builder builder = IntMap.builder(); + for (TypeCodec codec : codecs) { + builder.put(codec.getCqlType().getProtocolCode(), codec); + } + return builder.build(); + } + + // We call this after validating the types, so we know the cast will never fail. + private static TypeCodec uncheckedCast( + TypeCodec codec) { + @SuppressWarnings("unchecked") + TypeCodec result = (TypeCodec) codec; + return result; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/registry/CodecRegistryConstants.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/registry/CodecRegistryConstants.java new file mode 100644 index 00000000000..d52f79cc9f4 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/registry/CodecRegistryConstants.java @@ -0,0 +1,58 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec.registry; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; + +public class CodecRegistryConstants { + + /** + * The driver's default primitive codecs (map all primitive CQL types to their "natural" Java + * equivalent). + * + *

This is exposed in case you want to call {@link + * DefaultCodecRegistry#DefaultCodecRegistry(String, int, BiFunction, int, BiConsumer, + * TypeCodec[], TypeCodec[])} but only customize the caching options. + */ + public static final TypeCodec[] PRIMITIVE_CODECS = + new TypeCodec[] { + // Must be declared before AsciiCodec so it gets chosen when CQL type not available + TypeCodecs.TEXT, + // Must be declared before TimeUUIDCodec so it gets chosen when CQL type not available + TypeCodecs.UUID, + TypeCodecs.TIMEUUID, + TypeCodecs.TIMESTAMP, + TypeCodecs.INT, + TypeCodecs.BIGINT, + TypeCodecs.BLOB, + TypeCodecs.DOUBLE, + TypeCodecs.FLOAT, + TypeCodecs.DECIMAL, + TypeCodecs.VARINT, + TypeCodecs.INET, + TypeCodecs.BOOLEAN, + TypeCodecs.SMALLINT, + TypeCodecs.TINYINT, + TypeCodecs.DATE, + TypeCodecs.TIME, + TypeCodecs.DURATION, + TypeCodecs.COUNTER, + TypeCodecs.ASCII + }; +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/registry/DefaultCodecRegistry.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/registry/DefaultCodecRegistry.java new file mode 100644 index 00000000000..69ba447c7ed --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/codec/registry/DefaultCodecRegistry.java @@ -0,0 +1,154 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec.registry; + +import com.datastax.oss.driver.api.core.DriverExecutionException; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.base.Throwables; +import com.datastax.oss.driver.shaded.guava.common.cache.CacheBuilder; +import com.datastax.oss.driver.shaded.guava.common.cache.CacheLoader; +import com.datastax.oss.driver.shaded.guava.common.cache.LoadingCache; +import com.datastax.oss.driver.shaded.guava.common.cache.RemovalListener; +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.ExecutionError; +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.UncheckedExecutionException; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The default codec registry implementation. + * + *

It is a caching registry based on Guava cache (note that the driver shades Guava). + */ +@ThreadSafe +public class DefaultCodecRegistry extends CachingCodecRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultCodecRegistry.class); + + private final LoadingCache> cache; + + /** + * Creates a new instance, with some amount of control over the cache behavior. + * + *

Giving full access to the Guava cache API would be too much work, since it is shaded and we + * have to wrap everything. If you need something that's not available here, it's easy enough to + * write your own CachingCodecRegistry implementation. It's doubtful that stuff like cache + * eviction is that useful anyway. + */ + public DefaultCodecRegistry( + String logPrefix, + int initialCacheCapacity, + BiFunction, Integer> cacheWeigher, + int maximumCacheWeight, + BiConsumer> cacheRemovalListener, + TypeCodec[] primitiveCodecs, + TypeCodec[] userCodecs) { + + super(logPrefix, primitiveCodecs, userCodecs); + CacheBuilder cacheBuilder = CacheBuilder.newBuilder(); + if (initialCacheCapacity > 0) { + cacheBuilder.initialCapacity(initialCacheCapacity); + } + if (cacheWeigher != null) { + cacheBuilder.weigher(cacheWeigher::apply).maximumWeight(maximumCacheWeight); + } + CacheLoader> cacheLoader = + new CacheLoader>() { + @Override + public TypeCodec load(@NonNull CacheKey key) throws Exception { + return createCodec(key.cqlType, key.javaType, key.isJavaCovariant); + } + }; + if (cacheRemovalListener != null) { + this.cache = + cacheBuilder + .removalListener( + (RemovalListener>) + notification -> + cacheRemovalListener.accept( + notification.getKey(), notification.getValue())) + .build(cacheLoader); + } else { + this.cache = cacheBuilder.build(cacheLoader); + } + } + + public DefaultCodecRegistry(String logPrefix, TypeCodec... userCodecs) { + this(logPrefix, CodecRegistryConstants.PRIMITIVE_CODECS, userCodecs); + } + + public DefaultCodecRegistry( + String logPrefix, TypeCodec[] primitiveCodecs, TypeCodec... userCodecs) { + this(logPrefix, 0, null, 0, null, primitiveCodecs, userCodecs); + } + + @Override + protected TypeCodec getCachedCodec( + DataType cqlType, GenericType javaType, boolean isJavaCovariant) { + LOG.trace("[{}] Checking cache", logPrefix); + try { + return cache.getUnchecked(new CacheKey(cqlType, javaType, isJavaCovariant)); + } catch (UncheckedExecutionException | ExecutionError e) { + // unwrap exception cause and throw it directly. + Throwable cause = e.getCause(); + if (cause != null) { + Throwables.throwIfUnchecked(cause); + throw new DriverExecutionException(cause); + } else { + // Should never happen, throw just in case + throw new RuntimeException(e.getMessage()); + } + } + } + + public static final class CacheKey { + + public final DataType cqlType; + public final GenericType javaType; + public final boolean isJavaCovariant; + + public CacheKey(DataType cqlType, GenericType javaType, boolean isJavaCovariant) { + this.javaType = javaType; + this.cqlType = cqlType; + this.isJavaCovariant = isJavaCovariant; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof CacheKey) { + CacheKey that = (CacheKey) other; + return Objects.equals(this.cqlType, that.cqlType) + && Objects.equals(this.javaType, that.javaType) + && this.isJavaCovariant == that.isJavaCovariant; + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(cqlType, javaType, isJavaCovariant); + } + } +} diff --git a/driver-core/src/main/java/com/datastax/driver/core/VIntCoding.java b/core/src/main/java/com/datastax/oss/driver/internal/core/type/util/VIntCoding.java similarity index 85% rename from driver-core/src/main/java/com/datastax/driver/core/VIntCoding.java rename to core/src/main/java/com/datastax/oss/driver/internal/core/type/util/VIntCoding.java index 3113b658bdc..918949a13fc 100644 --- a/driver-core/src/main/java/com/datastax/driver/core/VIntCoding.java +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/type/util/VIntCoding.java @@ -42,9 +42,8 @@ // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -package com.datastax.driver.core; +package com.datastax.oss.driver.internal.core.type.util; -import io.netty.util.concurrent.FastThreadLocal; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; @@ -53,27 +52,28 @@ * Variable length encoding inspired from Google varints. * - *

- * *

Cassandra vints are encoded with the most significant group first. The most significant byte * will contains the information about how many extra bytes need to be read as well as the most - * significant bits of the integer. The number of extra bytes to read is encoded as 1 bits on the + * significant bits of the integer. The number of extra bytes to read is encoded as 1 bit on the * left side. For example, if we need to read 3 more bytes the first byte will start with 1110. If * the encoded integer is 8 bytes long the vint will be encoded on 9 bytes and the first byte will * be: 11111111 * - *

- * - *

Signed integer are (like protocol buffer varints) encoded using the ZigZag encoding so that + *

Signed integers are (like protocol buffer varints) encoded using the ZigZag encoding so that * numbers with a small absolute value have a small vint encoded value too. + * + *

Note that there is also a type called {@code varint} in the CQL protocol specification. This + * is completely unrelated. */ -class VIntCoding { +public class VIntCoding { private static long readUnsignedVInt(DataInput input) throws IOException { int firstByte = input.readByte(); // Bail out early if this is one byte, necessary or it fails later - if (firstByte >= 0) return firstByte; + if (firstByte >= 0) { + return firstByte; + } int size = numberOfExtraBytesToRead(firstByte); long retval = firstByte & firstByteValueMask(size); @@ -86,7 +86,7 @@ private static long readUnsignedVInt(DataInput input) throws IOException { return retval; } - static long readVInt(DataInput input) throws IOException { + public static long readVInt(DataInput input) throws IOException { return decodeZigZag64(readUnsignedVInt(input)); } @@ -97,9 +97,9 @@ private static int firstByteValueMask(int extraBytesToRead) { return 0xff >> extraBytesToRead; } - private static int encodeExtraBytesToRead(int extraBytesToRead) { + private static byte encodeExtraBytesToRead(int extraBytesToRead) { // because we have an extra bit in the value mask, we just need to invert it - return ~firstByteValueMask(extraBytesToRead); + return (byte) ~firstByteValueMask(extraBytesToRead); } private static int numberOfExtraBytesToRead(int firstByte) { @@ -110,13 +110,8 @@ private static int numberOfExtraBytesToRead(int firstByte) { return Integer.numberOfLeadingZeros(~firstByte) - 24; } - private static final FastThreadLocal encodingBuffer = - new FastThreadLocal() { - @Override - public byte[] initialValue() { - return new byte[9]; - } - }; + private static final ThreadLocal encodingBuffer = + ThreadLocal.withInitial(() -> new byte[9]); private static void writeUnsignedVInt(long value, DataOutput output) throws IOException { int size = VIntCoding.computeUnsignedVIntSize(value); @@ -140,7 +135,7 @@ private static byte[] encodeVInt(long value, int size) { return encodingSpace; } - static void writeVInt(long value, DataOutput output) throws IOException { + public static void writeVInt(long value, DataOutput output) throws IOException { writeUnsignedVInt(encodeZigZag64(value), output); } @@ -149,9 +144,9 @@ static void writeVInt(long value, DataOutput output) throws IOException { * efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64 bits * to be varint encoded, thus always taking 10 bytes on the wire.) * - * @param n An unsigned 64-bit integer, stored in a signed int because Java has no explicit + * @param n an unsigned 64-bit integer, stored in a signed int because Java has no explicit * unsigned support. - * @return A signed 64-bit integer. + * @return a signed 64-bit integer. */ private static long decodeZigZag64(final long n) { return (n >>> 1) ^ -(n & 1); @@ -162,8 +157,8 @@ private static long decodeZigZag64(final long n) { * efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64 bits * to be varint encoded, thus always taking 10 bytes on the wire.) * - * @param n A signed 64-bit integer. - * @return An unsigned 64-bit integer, stored in a signed int because Java has no explicit + * @param n a signed 64-bit integer. + * @return an unsigned 64-bit integer, stored in a signed int because Java has no explicit * unsigned support. */ private static long encodeZigZag64(final long n) { @@ -172,7 +167,7 @@ private static long encodeZigZag64(final long n) { } /** Compute the number of bytes that would be needed to encode a varint. */ - static int computeVIntSize(final long param) { + public static int computeVIntSize(final long param) { return computeUnsignedVIntSize(encodeZigZag64(param)); } @@ -180,7 +175,7 @@ static int computeVIntSize(final long param) { private static int computeUnsignedVIntSize(final long value) { int magnitude = Long.numberOfLeadingZeros( - value | 1); // | with 1 to ensure magntiude <= 63, so (63 - 1) / 7 <= 8 + value | 1); // | with 1 to ensure magnitude <= 63, so (63 - 1) / 7 <= 8 return (639 - magnitude * 9) >> 6; } } diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/ArrayUtils.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/ArrayUtils.java new file mode 100644 index 00000000000..25597e190c9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/ArrayUtils.java @@ -0,0 +1,104 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.ThreadLocalRandom; + +public class ArrayUtils { + + public static void swap(@NonNull ElementT[] elements, int i, int j) { + if (i != j) { + ElementT tmp = elements[i]; + elements[i] = elements[j]; + elements[j] = tmp; + } + } + + /** + * Moves an element towards the beginning of the array, shifting all the intermediary elements to + * the right (no-op if {@code targetIndex >= sourceIndex}). + */ + public static void bubbleUp( + @NonNull ElementT[] elements, int sourceIndex, int targetIndex) { + for (int i = sourceIndex; i > targetIndex; i--) { + swap(elements, i, i - 1); + } + } + + /** + * Moves an element towards the end of the array, shifting all the intermediary elements to the + * left (no-op if {@code targetIndex <= sourceIndex}). + */ + public static void bubbleDown( + @NonNull ElementT[] elements, int sourceIndex, int targetIndex) { + for (int i = sourceIndex; i < targetIndex; i++) { + swap(elements, i, i + 1); + } + } + + /** + * Shuffles the first n elements of the array in-place. + * + * @param elements the array to shuffle. + * @param n the number of elements to shuffle; must be {@code <= elements.length}. + * @see Modern + * Fisher-Yates shuffle + */ + public static void shuffleHead(@NonNull ElementT[] elements, int n) { + shuffleHead(elements, n, ThreadLocalRandom.current()); + } + + /** + * Shuffles the first n elements of the array in-place. + * + * @param elements the array to shuffle. + * @param n the number of elements to shuffle; must be {@code <= elements.length}. + * @param random the {@link ThreadLocalRandom} instance to use. This is mainly intended to + * facilitate tests. + * @see Modern + * Fisher-Yates shuffle + */ + public static void shuffleHead( + @NonNull ElementT[] elements, int n, @NonNull ThreadLocalRandom random) { + if (n > elements.length) { + throw new ArrayIndexOutOfBoundsException( + String.format( + "Can't shuffle the first %d elements, there are only %d", n, elements.length)); + } + if (n > 1) { + for (int i = n - 1; i > 0; i--) { + int j = random.nextInt(i + 1); + swap(elements, i, j); + } + } + } + + /** Rotates the elements in the specified range by the specified amount (round-robin). */ + public static void rotate( + @NonNull ElementT[] elements, int startIndex, int length, int amount) { + if (length >= 2) { + amount = amount % length; + // Repeatedly shift by 1. This is not the most time-efficient but the array will typically be + // small so we don't care, and this avoids allocating a temporary buffer. + for (int i = 0; i < amount; i++) { + bubbleDown(elements, startIndex, startIndex + length - 1); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/CountingIterator.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/CountingIterator.java new file mode 100644 index 00000000000..a65808e7b2a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/CountingIterator.java @@ -0,0 +1,118 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import java.util.Iterator; +import java.util.NoSuchElementException; +import net.jcip.annotations.NotThreadSafe; + +/** + * An iterator that knows in advance how many elements it will return, and maintains a counter as + * elements get returned. + */ +@NotThreadSafe +public abstract class CountingIterator implements Iterator { + + protected int remaining; + + public CountingIterator(int remaining) { + this.remaining = remaining; + } + + public int remaining() { + return remaining; + } + + /* + * The rest of this class was adapted from Guava's `AbstractIterator` (which we can't extend + * because its `next` method is final). Guava copyright notice follows: + * + * Copyright (C) 2007 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + private enum State { + READY, + NOT_READY, + DONE, + FAILED, + } + + private State state = State.NOT_READY; + private ElementT next; + + protected abstract ElementT computeNext(); + + protected final ElementT endOfData() { + state = State.DONE; + return null; + } + + @Override + public final boolean hasNext() { + Preconditions.checkState(state != State.FAILED); + switch (state) { + case DONE: + return false; + case READY: + return true; + default: + } + return tryToComputeNext(); + } + + private boolean tryToComputeNext() { + state = State.FAILED; // temporary pessimism + next = computeNext(); + if (state != State.DONE) { + state = State.READY; + return true; + } + return false; + } + + @Override + public final ElementT next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + state = State.NOT_READY; + ElementT result = next; + next = null; + // Added to original Guava code: decrement counter when we return an element + remaining -= 1; + return result; + } + + public final ElementT peek() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return next; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/DirectedGraph.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/DirectedGraph.java new file mode 100644 index 00000000000..6f75d759451 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/DirectedGraph.java @@ -0,0 +1,104 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.LinkedHashMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.driver.shaded.guava.common.collect.Maps; +import com.datastax.oss.driver.shaded.guava.common.collect.Multimap; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import net.jcip.annotations.NotThreadSafe; + +/** A basic directed graph implementation to perform topological sorts. */ +@NotThreadSafe +public class DirectedGraph { + + // We need to keep track of the predecessor count. For simplicity, use a map to store it + // alongside the vertices. + private final Map vertices; + private final Multimap adjacencyList; + private boolean wasSorted; + + public DirectedGraph(Collection vertices) { + this.vertices = Maps.newLinkedHashMapWithExpectedSize(vertices.size()); + this.adjacencyList = LinkedHashMultimap.create(); + + for (VertexT vertex : vertices) { + this.vertices.put(vertex, 0); + } + } + + @VisibleForTesting + @SafeVarargs + DirectedGraph(VertexT... vertices) { + this(Arrays.asList(vertices)); + } + + /** + * this assumes that {@code from} and {@code to} were part of the vertices passed to the + * constructor + */ + public void addEdge(VertexT from, VertexT to) { + Preconditions.checkArgument(vertices.containsKey(from) && vertices.containsKey(to)); + adjacencyList.put(from, to); + vertices.put(to, vertices.get(to) + 1); + } + + /** one-time use only, calling this multiple times on the same graph won't work */ + public List topologicalSort() { + Preconditions.checkState(!wasSorted); + wasSorted = true; + + Queue queue = new ArrayDeque<>(); + + for (Map.Entry entry : vertices.entrySet()) { + if (entry.getValue() == 0) { + queue.add(entry.getKey()); + } + } + + List result = Lists.newArrayList(); + while (!queue.isEmpty()) { + VertexT vertex = queue.remove(); + result.add(vertex); + for (VertexT successor : adjacencyList.get(vertex)) { + if (decrementAndGetCount(successor) == 0) { + queue.add(successor); + } + } + } + + if (result.size() != vertices.size()) { + throw new IllegalArgumentException("failed to perform topological sort, graph has a cycle"); + } + + return result; + } + + private int decrementAndGetCount(VertexT vertex) { + Integer count = vertices.get(vertex); + count = count - 1; + vertices.put(vertex, count); + return count; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/Loggers.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/Loggers.java new file mode 100644 index 00000000000..eeb753830bc --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/Loggers.java @@ -0,0 +1,41 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import org.slf4j.Logger; + +public class Loggers { + + /** + * Emits a warning log that includes an exception. If the current level is debug, the full stack + * trace is included, otherwise only the exception's message. + */ + public static void warnWithException(Logger logger, String format, Object... arguments) { + if (logger.isDebugEnabled()) { + logger.warn(format, arguments); + } else { + Object last = arguments[arguments.length - 1]; + if (last instanceof Throwable) { + Throwable t = (Throwable) last; + arguments[arguments.length - 1] = t.getClass().getSimpleName() + ": " + t.getMessage(); + logger.warn(format + " ({})", arguments); + } else { + // Should only be called with an exception as last argument, but handle gracefully anyway + logger.warn(format, arguments); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/NanoTime.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/NanoTime.java new file mode 100644 index 00000000000..9f4ec8bc978 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/NanoTime.java @@ -0,0 +1,53 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +public class NanoTime { + + private static final long ONE_HOUR = 3600L * 1000 * 1000 * 1000; + private static final long ONE_MINUTE = 60L * 1000 * 1000 * 1000; + private static final long ONE_SECOND = 1000 * 1000 * 1000; + private static final long ONE_MILLISECOND = 1000 * 1000; + private static final long ONE_MICROSECOND = 1000; + + /** Formats a duration in the best unit (truncating the fractional part). */ + public static String formatTimeSince(long startTimeNs) { + return format(System.nanoTime() - startTimeNs); + } + + /** Formats a duration in the best unit (truncating the fractional part). */ + public static String format(long elapsedNs) { + if (elapsedNs >= ONE_HOUR) { + long hours = elapsedNs / ONE_HOUR; + long minutes = (elapsedNs % ONE_HOUR) / ONE_MINUTE; + return hours + " h " + minutes + " mn"; + } else if (elapsedNs >= ONE_MINUTE) { + long minutes = elapsedNs / ONE_MINUTE; + long seconds = (elapsedNs % ONE_MINUTE) / ONE_SECOND; + return minutes + " mn " + seconds + " s"; + } else if (elapsedNs >= ONE_SECOND) { + long seconds = elapsedNs / ONE_SECOND; + long milliseconds = (elapsedNs % ONE_SECOND) / ONE_MILLISECOND; + return seconds + "." + milliseconds + " s"; + } else if (elapsedNs >= ONE_MILLISECOND) { + return (elapsedNs / ONE_MILLISECOND) + " ms"; + } else if (elapsedNs >= ONE_MICROSECOND) { + return (elapsedNs / ONE_MICROSECOND) + " us"; + } else { + return elapsedNs + " ns"; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/ProtocolUtils.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/ProtocolUtils.java new file mode 100644 index 00000000000..06b47479eee --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/ProtocolUtils.java @@ -0,0 +1,114 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import com.datastax.oss.protocol.internal.ProtocolConstants; + +public class ProtocolUtils { + /** + * Formats a message opcode for logs and error messages. + * + *

Note that the reason why we don't use enums is because the driver can be extended with + * custom opcodes. + */ + public static String opcodeString(int opcode) { + switch (opcode) { + case ProtocolConstants.Opcode.ERROR: + return "ERROR"; + case ProtocolConstants.Opcode.STARTUP: + return "STARTUP"; + case ProtocolConstants.Opcode.READY: + return "READY"; + case ProtocolConstants.Opcode.AUTHENTICATE: + return "AUTHENTICATE"; + case ProtocolConstants.Opcode.OPTIONS: + return "OPTIONS"; + case ProtocolConstants.Opcode.SUPPORTED: + return "SUPPORTED"; + case ProtocolConstants.Opcode.QUERY: + return "QUERY"; + case ProtocolConstants.Opcode.RESULT: + return "RESULT"; + case ProtocolConstants.Opcode.PREPARE: + return "PREPARE"; + case ProtocolConstants.Opcode.EXECUTE: + return "EXECUTE"; + case ProtocolConstants.Opcode.REGISTER: + return "REGISTER"; + case ProtocolConstants.Opcode.EVENT: + return "EVENT"; + case ProtocolConstants.Opcode.BATCH: + return "BATCH"; + case ProtocolConstants.Opcode.AUTH_CHALLENGE: + return "AUTH_CHALLENGE"; + case ProtocolConstants.Opcode.AUTH_RESPONSE: + return "AUTH_RESPONSE"; + case ProtocolConstants.Opcode.AUTH_SUCCESS: + return "AUTH_SUCCESS"; + default: + return "0x" + Integer.toHexString(opcode); + } + } + + /** + * Formats an error code for logs and error messages. + * + *

Note that the reason why we don't use enums is because the driver can be extended with + * custom codes. + */ + public static String errorCodeString(int errorCode) { + switch (errorCode) { + case ProtocolConstants.ErrorCode.SERVER_ERROR: + return "SERVER_ERROR"; + case ProtocolConstants.ErrorCode.PROTOCOL_ERROR: + return "PROTOCOL_ERROR"; + case ProtocolConstants.ErrorCode.AUTH_ERROR: + return "AUTH_ERROR"; + case ProtocolConstants.ErrorCode.UNAVAILABLE: + return "UNAVAILABLE"; + case ProtocolConstants.ErrorCode.OVERLOADED: + return "OVERLOADED"; + case ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING: + return "IS_BOOTSTRAPPING"; + case ProtocolConstants.ErrorCode.TRUNCATE_ERROR: + return "TRUNCATE_ERROR"; + case ProtocolConstants.ErrorCode.WRITE_TIMEOUT: + return "WRITE_TIMEOUT"; + case ProtocolConstants.ErrorCode.READ_TIMEOUT: + return "READ_TIMEOUT"; + case ProtocolConstants.ErrorCode.READ_FAILURE: + return "READ_FAILURE"; + case ProtocolConstants.ErrorCode.FUNCTION_FAILURE: + return "FUNCTION_FAILURE"; + case ProtocolConstants.ErrorCode.WRITE_FAILURE: + return "WRITE_FAILURE"; + case ProtocolConstants.ErrorCode.SYNTAX_ERROR: + return "SYNTAX_ERROR"; + case ProtocolConstants.ErrorCode.UNAUTHORIZED: + return "UNAUTHORIZED"; + case ProtocolConstants.ErrorCode.INVALID: + return "INVALID"; + case ProtocolConstants.ErrorCode.CONFIG_ERROR: + return "CONFIG_ERROR"; + case ProtocolConstants.ErrorCode.ALREADY_EXISTS: + return "ALREADY_EXISTS"; + case ProtocolConstants.ErrorCode.UNPREPARED: + return "UNPREPARED"; + default: + return "0x" + Integer.toHexString(errorCode); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/Reflection.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/Reflection.java new file mode 100644 index 00000000000..d57e23c3982 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/Reflection.java @@ -0,0 +1,243 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.config.DriverOption; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.shaded.guava.common.base.Joiner; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ListMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.MultimapBuilder; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Reflection { + + private static final Logger LOG = LoggerFactory.getLogger(Reflection.class); + + /** + * Loads a class by name. + * + *

This methods tries first with the current thread's context class loader (the intent is that + * if the driver is in a low-level loader of an application server -- e.g. bootstrap or system -- + * it can still find classes in the application's class loader). If it is null, it defaults to the + * class loader that loaded the class calling this method. + * + * @return null if the class does not exist. + */ + public static Class loadClass(ClassLoader classLoader, String className) { + try { + // If input classLoader is null, use current thread's ClassLoader, if that is null, use + // default (calling class') ClassLoader. + ClassLoader cl = + classLoader != null ? classLoader : Thread.currentThread().getContextClassLoader(); + if (cl != null) { + return Class.forName(className, true, cl); + } else { + return Class.forName(className); + } + } catch (ClassNotFoundException e) { + return null; + } + } + + /** + * Tries to create an instance of a class, given an option defined in the driver configuration. + * + *

For example: + * + *

+   * my-policy.class = my.package.MyPolicyImpl
+   * 
+ * + * The class will be instantiated via reflection, it must have a constructor that takes a {@link + * DriverContext} argument. + * + * @param context the driver context. + * @param classNameOption the option that indicates the class. It will be looked up in the default + * profile of the configuration stored in the context. + * @param expectedSuperType a super-type that the class is expected to implement/extend. + * @param defaultPackages the default packages to prepend to the class name if it's not qualified. + * They will be tried in order, the first one that matches an existing class will be used. + * @return the new instance, or empty if {@code classNameOption} is not defined in the + * configuration. + */ + public static Optional buildFromConfig( + InternalDriverContext context, + DriverOption classNameOption, + Class expectedSuperType, + String... defaultPackages) { + return buildFromConfig(context, null, classNameOption, expectedSuperType, defaultPackages); + } + + /** + * Tries to create multiple instances of a class, given options defined in the driver + * configuration and possibly overridden in profiles. + * + *

For example: + * + *

+   * my-policy.class = package1.PolicyImpl1
+   * profiles {
+   *   my-profile { my-policy.class = package2.PolicyImpl2 }
+   * }
+   * 
+ * + * The class will be instantiated via reflection, it must have a constructor that takes two + * arguments: the {@link DriverContext}, and a string representing the profile name. + * + *

This method assumes the policy is mandatory, the class option must be present at least for + * the default profile. + * + * @param context the driver context. + * @param rootOption the root option for the policy (my-policy in the example above). The class + * name is assumed to be in a 'class' child option. + * @param expectedSuperType a super-type that the class is expected to implement/extend. + * @param defaultPackages the default packages to prepend to the class name if it's not qualified. + * They will be tried in order, the first one that matches an existing class will be used. + * @return the policy instances by profile name. If multiple profiles share the same + * configuration, a single instance will be shared by all their entries. + */ + public static Map buildFromConfigProfiles( + InternalDriverContext context, + DriverOption rootOption, + Class expectedSuperType, + String... defaultPackages) { + + // Find out how many distinct configurations we have + ListMultimap profilesByConfig = + MultimapBuilder.hashKeys().arrayListValues().build(); + for (DriverExecutionProfile profile : context.getConfig().getProfiles().values()) { + profilesByConfig.put(profile.getComparisonKey(rootOption), profile.getName()); + } + + // Instantiate each distinct configuration, and associate it with the corresponding profiles + ImmutableMap.Builder result = ImmutableMap.builder(); + for (Collection profiles : profilesByConfig.asMap().values()) { + // Since all profiles use the same config, we can use any of them + String profileName = profiles.iterator().next(); + ComponentT policy = + buildFromConfig( + context, profileName, classOption(rootOption), expectedSuperType, defaultPackages) + .orElseThrow( + () -> + new IllegalArgumentException( + String.format( + "Missing configuration for %s in profile %s", + rootOption.getPath(), profileName))); + for (String profile : profiles) { + result.put(profile, policy); + } + } + return result.build(); + } + + /** + * @param profileName if null, this is a global policy, use the default profile and look for a + * one-arg constructor. If not null, this is a per-profile policy, look for a two-arg + * constructor. + */ + public static Optional buildFromConfig( + InternalDriverContext context, + String profileName, + DriverOption classNameOption, + Class expectedSuperType, + String... defaultPackages) { + + DriverExecutionProfile config = + (profileName == null) + ? context.getConfig().getDefaultProfile() + : context.getConfig().getProfile(profileName); + + String configPath = classNameOption.getPath(); + LOG.debug("Creating a {} from config option {}", expectedSuperType.getSimpleName(), configPath); + + if (!config.isDefined(classNameOption)) { + LOG.debug("Option is not defined, skipping"); + return Optional.empty(); + } + + String className = config.getString(classNameOption); + Class clazz = null; + if (className.contains(".")) { + LOG.debug("Building from fully-qualified name {}", className); + clazz = loadClass(context.getClassLoader(), className); + } else { + LOG.debug("Building from unqualified name {}", className); + for (String defaultPackage : defaultPackages) { + String qualifiedClassName = defaultPackage + "." + className; + LOG.debug("Trying with default package {}", qualifiedClassName); + clazz = loadClass(context.getClassLoader(), qualifiedClassName); + if (clazz != null) { + break; + } + } + } + if (clazz == null) { + throw new IllegalArgumentException( + String.format("Can't find class %s (specified by %s)", className, configPath)); + } + Preconditions.checkArgument( + expectedSuperType.isAssignableFrom(clazz), + "Expected class %s (specified by %s) to be a subtype of %s", + className, + configPath, + expectedSuperType.getName()); + + Constructor constructor; + Class[] argumentTypes = + (profileName == null) + ? new Class[] {DriverContext.class} + : new Class[] {DriverContext.class, String.class}; + try { + constructor = clazz.asSubclass(expectedSuperType).getConstructor(argumentTypes); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException( + String.format( + "Expected class %s (specified by %s) " + + "to have an accessible constructor with arguments (%s)", + className, configPath, Joiner.on(',').join(argumentTypes))); + } + try { + @SuppressWarnings("JavaReflectionInvocation") + ComponentT instance = + (profileName == null) + ? constructor.newInstance(context) + : constructor.newInstance(context, profileName); + return Optional.of(instance); + } catch (Exception e) { + // ITE just wraps an exception thrown by the constructor, get rid of it: + Throwable cause = (e instanceof InvocationTargetException) ? e.getCause() : e; + throw new IllegalArgumentException( + String.format( + "Error instantiating class %s (specified by %s): %s", + className, configPath, cause.getMessage()), + cause); + } + } + + private static DriverOption classOption(DriverOption rootOption) { + return () -> rootOption.getPath() + ".class"; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/RoutingKey.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/RoutingKey.java new file mode 100644 index 00000000000..fc1ef249edd --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/RoutingKey.java @@ -0,0 +1,46 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; + +public class RoutingKey { + + /** Assembles multiple routing key components into a single buffer. */ + @NonNull + public static ByteBuffer compose(@NonNull ByteBuffer... components) { + if (components.length == 1) return components[0]; + + int totalLength = 0; + for (ByteBuffer bb : components) totalLength += 2 + bb.remaining() + 1; + + ByteBuffer out = ByteBuffer.allocate(totalLength); + for (ByteBuffer buffer : components) { + ByteBuffer bb = buffer.duplicate(); + putShortLength(out, bb.remaining()); + out.put(bb); + out.put((byte) 0); + } + out.flip(); + return out; + } + + private static void putShortLength(ByteBuffer bb, int length) { + bb.put((byte) ((length >> 8) & 0xFF)); + bb.put((byte) (length & 0xFF)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/Sizes.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/Sizes.java new file mode 100644 index 00000000000..0b52a18e801 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/Sizes.java @@ -0,0 +1,136 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.cql.BatchableStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.cql.Conversions; +import com.datastax.oss.protocol.internal.FrameCodec; +import com.datastax.oss.protocol.internal.PrimitiveSizes; +import com.datastax.oss.protocol.internal.request.query.QueryOptions; +import com.datastax.oss.protocol.internal.request.query.Values; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Sizes { + + /** Returns a common size for all kinds of Request implementations. */ + public static int minimumRequestSize(Request request) { + + // Header and payload are common inside a Frame at the protocol level + + // Frame header has a fixed size of 9 for protocol version >= V3, which includes Frame flags + // size + int size = FrameCodec.headerEncodedSize(); + + if (!request.getCustomPayload().isEmpty()) { + // Custom payload is not supported in v3, but assume user won't have a custom payload set if + // they use this version + size += PrimitiveSizes.sizeOfBytesMap(request.getCustomPayload()); + } + + return size; + } + + public static int minimumStatementSize(Statement statement, DriverContext context) { + int size = minimumRequestSize(statement); + + // These are options in the protocol inside a frame that are common to all Statements + + size += QueryOptions.queryFlagsSize(context.getProtocolVersion().getCode()); + + size += PrimitiveSizes.SHORT; // size of consistency level + size += PrimitiveSizes.SHORT; // size of serial consistency level + + return size; + } + + /** + * Returns the size in bytes of a simple statement's values, depending on whether the values are + * named or positional. + */ + public static int sizeOfSimpleStatementValues( + SimpleStatement simpleStatement, + ProtocolVersion protocolVersion, + CodecRegistry codecRegistry) { + int size = 0; + + if (!simpleStatement.getPositionalValues().isEmpty()) { + + List positionalValues = + new ArrayList<>(simpleStatement.getPositionalValues().size()); + for (Object value : simpleStatement.getPositionalValues()) { + positionalValues.add(Conversions.encode(value, codecRegistry, protocolVersion)); + } + + size += Values.sizeOfPositionalValues(positionalValues); + + } else if (!simpleStatement.getNamedValues().isEmpty()) { + + Map namedValues = new HashMap<>(simpleStatement.getNamedValues().size()); + for (Map.Entry value : simpleStatement.getNamedValues().entrySet()) { + namedValues.put( + value.getKey().asInternal(), + Conversions.encode(value.getValue(), codecRegistry, protocolVersion)); + } + + size += Values.sizeOfNamedValues(namedValues); + } + return size; + } + + /** Return the size in bytes of a bound statement's values. */ + public static int sizeOfBoundStatementValues(BoundStatement boundStatement) { + return Values.sizeOfPositionalValues(boundStatement.getValues()); + } + + /** + * The size of a statement inside a batch query is different from the size of a complete + * Statement. The inner batch statements only include the query or prepared ID, and the values of + * the statement. + */ + public static Integer sizeOfInnerBatchStatementInBytes( + BatchableStatement statement, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { + int size = 0; + + size += + PrimitiveSizes + .BYTE; // for each inner statement, there is one byte for the "kind": prepared or string + + if (statement instanceof SimpleStatement) { + size += PrimitiveSizes.sizeOfLongString(((SimpleStatement) statement).getQuery()); + size += + sizeOfSimpleStatementValues( + ((SimpleStatement) statement), protocolVersion, codecRegistry); + } else if (statement instanceof BoundStatement) { + size += + PrimitiveSizes.sizeOfShortBytes( + ((BoundStatement) statement).getPreparedStatement().getId().array()); + size += sizeOfBoundStatementValues(((BoundStatement) statement)); + } + return size; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/Strings.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/Strings.java new file mode 100644 index 00000000000..50063799a8e --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/Strings.java @@ -0,0 +1,317 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; + +public class Strings { + + /** + * Return {@code true} if the given string is surrounded by single quotes, and {@code false} + * otherwise. + * + * @param value The string to inspect. + * @return {@code true} if the given string is surrounded by single quotes, and {@code false} + * otherwise. + */ + public static boolean isQuoted(String value) { + return isQuoted(value, '\''); + } + + /** + * Quote the given string; single quotes are escaped. If the given string is null, this method + * returns a quoted empty string ({@code ''}). + * + * @param value The value to quote. + * @return The quoted string. + */ + public static String quote(String value) { + return quote(value, '\''); + } + + /** + * Unquote the given string if it is quoted; single quotes are unescaped. If the given string is + * not quoted, it is returned without any modification. + * + * @param value The string to unquote. + * @return The unquoted string. + */ + public static String unquote(String value) { + return unquote(value, '\''); + } + + /** + * Return {@code true} if the given string is surrounded by double quotes, and {@code false} + * otherwise. + * + * @param value The string to inspect. + * @return {@code true} if the given string is surrounded by double quotes, and {@code false} + * otherwise. + */ + public static boolean isDoubleQuoted(String value) { + return isQuoted(value, '\"'); + } + + /** + * Double quote the given string; double quotes are escaped. If the given string is null, this + * method returns a quoted empty string ({@code ""}). + * + * @param value The value to double quote. + * @return The double quoted string. + */ + public static String doubleQuote(String value) { + return quote(value, '"'); + } + + /** + * Unquote the given string if it is double quoted; double quotes are unescaped. If the given + * string is not double quoted, it is returned without any modification. + * + * @param value The string to un-double quote. + * @return The un-double quoted string. + */ + public static String unDoubleQuote(String value) { + return unquote(value, '"'); + } + + /** Whether a string needs double quotes to be a valid CQL identifier. */ + public static boolean needsDoubleQuotes(String s) { + // this method should only be called for C*-provided identifiers, + // so we expect it to be non-null and non-empty. + assert s != null && !s.isEmpty(); + char c = s.charAt(0); + if (!(c >= 97 && c <= 122)) // a-z + return true; + for (int i = 1; i < s.length(); i++) { + c = s.charAt(i); + if (!((c >= 48 && c <= 57) // 0-9 + || (c == 95) // _ + || (c >= 97 && c <= 122) // a-z + )) { + return true; + } + } + return isReservedCqlKeyword(s); + } + + /** + * Return {@code true} if the given string is surrounded by the quote character given, and {@code + * false} otherwise. + * + * @param value The string to inspect. + * @return {@code true} if the given string is surrounded by the quote character, and {@code + * false} otherwise. + */ + private static boolean isQuoted(String value, char quoteChar) { + return value != null + && value.length() > 1 + && value.charAt(0) == quoteChar + && value.charAt(value.length() - 1) == quoteChar; + } + + /** + * @param quoteChar " or ' + * @return A quoted empty string. + */ + private static String emptyQuoted(char quoteChar) { + // don't handle non quote characters, this is done so that these are interned and don't create + // repeated empty quoted strings. + assert quoteChar == '"' || quoteChar == '\''; + if (quoteChar == '"') return "\"\""; + else return "''"; + } + + /** + * Quotes text and escapes any existing quotes in the text. {@code String.replace()} is a bit too + * inefficient (see JAVA-67, JAVA-1262). + * + * @param text The text. + * @param quoteChar The character to use as a quote. + * @return The text with surrounded in quotes with all existing quotes escaped with (i.e. ' + * becomes '') + */ + private static String quote(String text, char quoteChar) { + if (text == null || text.isEmpty()) return emptyQuoted(quoteChar); + + int nbMatch = 0; + int start = -1; + do { + start = text.indexOf(quoteChar, start + 1); + if (start != -1) ++nbMatch; + } while (start != -1); + + // no quotes found that need to be escaped, simply surround in quotes and return. + if (nbMatch == 0) return quoteChar + text + quoteChar; + + // 2 for beginning and end quotes. + // length for original text + // nbMatch for escape characters to add to quotes to be escaped. + int newLength = 2 + text.length() + nbMatch; + char[] result = new char[newLength]; + result[0] = quoteChar; + result[newLength - 1] = quoteChar; + int newIdx = 1; + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == quoteChar) { + // escape quote with another occurrence. + result[newIdx++] = c; + result[newIdx++] = c; + } else { + result[newIdx++] = c; + } + } + return new String(result); + } + + /** + * Unquotes text and unescapes non surrounding quotes. {@code String.replace()} is a bit too + * inefficient (see JAVA-67, JAVA-1262). + * + * @param text The text + * @param quoteChar The character to use as a quote. + * @return The text with surrounding quotes removed and non surrounding quotes unescaped (i.e. '' + * becomes ') + */ + private static String unquote(String text, char quoteChar) { + if (!isQuoted(text, quoteChar)) return text; + + if (text.length() == 2) return ""; + + String search = emptyQuoted(quoteChar); + int nbMatch = 0; + int start = -1; + do { + start = text.indexOf(search, start + 2); + // ignore the second to last character occurrence, as the last character is a quote. + if (start != -1 && start != text.length() - 2) ++nbMatch; + } while (start != -1); + + // no escaped quotes found, simply remove surrounding quotes and return. + if (nbMatch == 0) return text.substring(1, text.length() - 1); + + // length of the new string will be its current length - the number of occurrences. + int newLength = text.length() - nbMatch - 2; + char[] result = new char[newLength]; + int newIdx = 0; + // track whenever a quoteChar is encountered and the previous character is not a quoteChar. + boolean firstFound = false; + for (int i = 1; i < text.length() - 1; i++) { + char c = text.charAt(i); + if (c == quoteChar) { + if (firstFound) { + // The previous character was a quoteChar, don't add this to result, this action in + // effect removes consecutive quotes. + firstFound = false; + } else { + // found a quoteChar and the previous character was not a quoteChar, include in result. + firstFound = true; + result[newIdx++] = c; + } + } else { + // non quoteChar encountered, include in result. + result[newIdx++] = c; + firstFound = false; + } + } + return new String(result); + } + + private static boolean isReservedCqlKeyword(String id) { + return id != null && RESERVED_KEYWORDS.contains(id.toLowerCase()); + } + + /** + * Check whether the given string corresponds to a valid CQL long literal. Long literals are + * composed solely by digits, but can have an optional leading minus sign. + * + * @param str The string to inspect. + * @return {@code true} if the given string corresponds to a valid CQL integer literal, {@code + * false} otherwise. + */ + public static boolean isLongLiteral(String str) { + if (str == null || str.isEmpty()) return false; + char[] chars = str.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if ((c < '0' && (i != 0 || c != '-')) || c > '9') return false; + } + return true; + } + + private Strings() {} + + private static final ImmutableSet RESERVED_KEYWORDS = + ImmutableSet.of( + // See https://github.com/apache/cassandra/blob/trunk/doc/cql3/CQL.textile#appendixA + "add", + "allow", + "alter", + "and", + "any", + "apply", + "asc", + "authorize", + "batch", + "begin", + "by", + "columnfamily", + "create", + "delete", + "desc", + "drop", + "each_quorum", + "from", + "grant", + "in", + "index", + "inet", + "infinity", + "insert", + "into", + "keyspace", + "keyspaces", + "limit", + "local_one", + "local_quorum", + "modify", + "nan", + "norecursive", + "of", + "on", + "one", + "order", + "password", + "primary", + "quorum", + "rename", + "revoke", + "schema", + "select", + "set", + "table", + "to", + "token", + "three", + "truncate", + "two", + "unlogged", + "update", + "use", + "using", + "where", + "with"); +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/collection/QueryPlan.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/collection/QueryPlan.java new file mode 100644 index 00000000000..dfe2a45757f --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/collection/QueryPlan.java @@ -0,0 +1,112 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.collection; + +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.internal.core.loadbalancing.DefaultLoadBalancingPolicy; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterators; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.AbstractCollection; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicInteger; +import net.jcip.annotations.ThreadSafe; + +/** + * A specialized, thread-safe queue implementation for {@link + * LoadBalancingPolicy#newQueryPlan(Request, Session)}. + * + *

All nodes must be provided at construction time. After that, the only valid mutation operation + * is {@link #poll()}, other methods throw. + * + *

This class is not a general-purpose implementation, it is tailored for a specific use case in + * the driver. It makes a few unconventional API choices for the sake of performance (see {@link + * #QueryPlan(Object...)}. It can be reused for custom load balancing policies; if you plan to do + * so, study the source code of {@link DefaultLoadBalancingPolicy}. + */ +@ThreadSafe +public class QueryPlan extends AbstractCollection implements Queue { + + private final Object[] nodes; + private final AtomicInteger nextIndex = new AtomicInteger(); + + /** + * @param nodes the nodes to initially fill the queue with. For efficiency, there is no defensive + * copy, the provided array is used directly. The declared type is {@code Object[]} because of + * implementation details of {@link DefaultLoadBalancingPolicy}, but all elements must be + * instances of {@link Node}, otherwise instance methods will fail later. + */ + public QueryPlan(@NonNull Object... nodes) { + this.nodes = nodes; + } + + @Nullable + @Override + public Node poll() { + // We don't handle overflow. In practice it won't be an issue, since the driver stops polling + // once the query plan is empty. + int i = nextIndex.getAndIncrement(); + return (i >= nodes.length) ? null : (Node) nodes[i]; + } + + /** + * {@inheritDoc} + * + *

The returned iterator reflects the state of the queue at the time of the call, and is not + * affected by further modifications. + */ + @NonNull + @Override + public Iterator iterator() { + int i = nextIndex.get(); + if (i >= nodes.length) { + return Collections.emptyList().iterator(); + } else { + return Iterators.forArray(Arrays.copyOfRange(nodes, i, nodes.length, Node[].class)); + } + } + + @Override + public int size() { + return Math.max(nodes.length - nextIndex.get(), 0); + } + + @Override + public boolean offer(Node node) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Node remove() { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Node element() { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Node peek() { + throw new UnsupportedOperationException("Not implemented"); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/BlockingOperation.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/BlockingOperation.java new file mode 100644 index 00000000000..7797594b7b9 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/BlockingOperation.java @@ -0,0 +1,67 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.util.concurrent.FastThreadLocalThread; +import java.util.concurrent.ThreadFactory; + +/** + * Safeguards against bad usage patterns in client code that could introduce deadlocks in the + * driver. + * + *

The driver internals are fully asynchronous, nothing should ever block. On the other hand, our + * API exposes synchronous wrappers, that call async methods and wait on the result (as a + * convenience for clients that don't want to do async). These methods should never be called on a + * driver thread, because this can lead to deadlocks. This can happen from client code if it uses + * callbacks. + */ +public class BlockingOperation { + + /** + * This method is invoked from each synchronous driver method, and checks that we are not on a + * driver thread. + * + *

For this to work, all driver threads must be created by {@link SafeThreadFactory} (which is + * the case by default). + * + * @throws IllegalStateException if a driver thread is executing this. + */ + public static void checkNotDriverThread() { + if (Thread.currentThread() instanceof InternalThread) { + throw new IllegalStateException( + "Detected a synchronous API call on a driver thread, " + + "failing because this can cause deadlocks."); + } + } + + /** + * Marks threads as driver threads, so that they will be detected by {@link + * #checkNotDriverThread()} + */ + public static class SafeThreadFactory implements ThreadFactory { + @Override + public Thread newThread(@NonNull Runnable r) { + return new InternalThread(r); + } + } + + private static class InternalThread extends FastThreadLocalThread { + private InternalThread(Runnable runnable) { + super(runnable); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/CompletableFutures.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/CompletableFutures.java new file mode 100644 index 00000000000..84bbb68b986 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/CompletableFutures.java @@ -0,0 +1,156 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import com.datastax.oss.driver.api.core.DriverException; +import com.datastax.oss.driver.api.core.DriverExecutionException; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +public class CompletableFutures { + + public static CompletableFuture failedFuture(Throwable cause) { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(cause); + return future; + } + + /** Completes {@code target} with the outcome of {@code source}. */ + public static void completeFrom(CompletionStage source, CompletableFuture target) { + source.whenComplete( + (t, error) -> { + if (error != null) { + target.completeExceptionally(error); + } else { + target.complete(t); + } + }); + } + + /** @return a completion stage that completes when all inputs are done (success or failure). */ + public static CompletionStage allDone(List> inputs) { + CompletableFuture result = new CompletableFuture<>(); + if (inputs.isEmpty()) { + result.complete(null); + } else { + final int todo = inputs.size(); + final AtomicInteger done = new AtomicInteger(); + for (CompletionStage input : inputs) { + input.whenComplete( + (v, error) -> { + if (done.incrementAndGet() == todo) { + result.complete(null); + } + }); + } + } + return result; + } + + /** Do something when all inputs are done (success or failure). */ + public static void whenAllDone( + List> inputs, Runnable callback, Executor executor) { + allDone(inputs).thenRunAsync(callback, executor).exceptionally(UncaughtExceptions::log); + } + + /** Get the result now, when we know for sure that the future is complete. */ + public static T getCompleted(CompletionStage stage) { + CompletableFuture future = stage.toCompletableFuture(); + Preconditions.checkArgument(future.isDone() && !future.isCompletedExceptionally()); + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + // Neither can happen given the precondition + throw new AssertionError("Unexpected error", e); + } + } + + /** Get the error now, when we know for sure that the future is failed. */ + public static Throwable getFailed(CompletionStage stage) { + CompletableFuture future = stage.toCompletableFuture(); + Preconditions.checkArgument(future.isCompletedExceptionally()); + try { + future.get(); + throw new AssertionError("future should be failed"); + } catch (InterruptedException e) { + throw new AssertionError("Unexpected error", e); + } catch (ExecutionException e) { + return e.getCause(); + } + } + + public static T getUninterruptibly(CompletionStage stage) { + boolean interrupted = false; + try { + while (true) { + try { + return stage.toCompletableFuture().get(); + } catch (InterruptedException e) { + interrupted = true; + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof DriverException) { + throw ((DriverException) cause).copy(); + } + throw new DriverExecutionException(cause); + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } + + /** + * Executes a function on the calling thread and returns result in a {@link CompletableFuture}. + * + *

Similar to {@link CompletableFuture#completedFuture} except takes a {@link Supplier} and if + * the supplier throws an unchecked exception, the returning future fails with that exception. + * + * @param supplier Function to execute + * @param Type of result + * @return result of function wrapped in future + */ + public static CompletableFuture wrap(Supplier supplier) { + try { + return CompletableFuture.completedFuture(supplier.get()); + } catch (Throwable t) { + return failedFuture(t); + } + } + + public static void whenCancelled(CompletionStage stage, Runnable action) { + stage.exceptionally( + (error) -> { + if (error instanceof CancellationException) { + action.run(); + } + return null; + }); + } + + public static void propagateCancellation(CompletionStage source, CompletionStage target) { + whenCancelled(source, () -> target.toCompletableFuture().cancel(true)); + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/CycleDetector.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/CycleDetector.java new file mode 100644 index 00000000000..25fb3a4f8d8 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/CycleDetector.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.graph.Graphs; +import com.datastax.oss.driver.shaded.guava.common.graph.MutableValueGraph; +import com.datastax.oss.driver.shaded.guava.common.graph.ValueGraphBuilder; +import net.jcip.annotations.ThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Detects cycles between a set of {@link LazyReference} instances. */ +@ThreadSafe +public class CycleDetector { + private static final boolean ENABLED = + Boolean.getBoolean("com.datastax.oss.driver.DETECT_CYCLES"); + private static final Logger LOG = LoggerFactory.getLogger(CycleDetector.class); + + private final String errorMessage; + private final boolean enabled; + private final MutableValueGraph graph; + + public CycleDetector(String errorMessage) { + this(errorMessage, ENABLED); + } + + @VisibleForTesting + CycleDetector(String errorMessage, boolean enabled) { + this.errorMessage = errorMessage; + this.enabled = enabled; + this.graph = enabled ? ValueGraphBuilder.directed().build() : null; + } + + void onTryLock(LazyReference reference) { + if (enabled) { + synchronized (this) { + Thread me = Thread.currentThread(); + LOG.debug("{} wants to initialize {}", me, reference.getName()); + graph.putEdgeValue(me.getName(), reference.getName(), "wants to initialize"); + LOG.debug("{}", graph); + if (Graphs.hasCycle(graph.asGraph())) { + throw new IllegalStateException(errorMessage + " " + graph); + } + } + } + } + + void onLockAcquired(LazyReference reference) { + if (enabled) { + synchronized (this) { + Thread me = Thread.currentThread(); + LOG.debug("{} is initializing {}", me, reference.getName()); + String old = graph.removeEdge(me.getName(), reference.getName()); + assert "wants to initialize".equals(old); + graph.putEdgeValue(reference.getName(), me.getName(), "is getting initialized by"); + } + } + } + + void onReleaseLock(LazyReference reference) { + if (enabled) { + synchronized (this) { + Thread me = Thread.currentThread(); + LOG.debug("{} is done initializing {}", me, reference.getName()); + graph.removeEdge(reference.getName(), me.getName()); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/Debouncer.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/Debouncer.java new file mode 100644 index 00000000000..ded770a3d48 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/Debouncer.java @@ -0,0 +1,146 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.ScheduledFuture; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Function; +import net.jcip.annotations.NotThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Debounces a sequence of events to smoothen temporary oscillations. + * + *

When a first event is received, the debouncer starts a time window. If no other event is + * received within that window, the initial event is flushed. However, if another event arrives, the + * window is reset, and the next flush will now contain both events. If the window keeps getting + * reset, the debouncer will flush after a given number of accumulated events. + * + * @param the type of the incoming events. + * @param the resulting type after the events of a batch have been coalesced. + */ +@NotThreadSafe // must be confined to adminExecutor +public class Debouncer { + private static final Logger LOG = LoggerFactory.getLogger(Debouncer.class); + + private final EventExecutor adminExecutor; + private final Consumer onFlush; + private final Duration window; + private final long maxEvents; + private final Function, CoalescedT> coalescer; + + private List currentBatch = new ArrayList<>(); + private ScheduledFuture nextFlush; + private boolean stopped; + + /** + * Creates a new instance. + * + * @param adminExecutor the executor that will be used to schedule all tasks. + * @param coalescer how to transform a batch of events into a result. + * @param onFlush what to do with a result. + * @param window the time window. + * @param maxEvents the maximum number of accumulated events before a flush is forced. + */ + public Debouncer( + EventExecutor adminExecutor, + Function, CoalescedT> coalescer, + Consumer onFlush, + Duration window, + long maxEvents) { + this.coalescer = coalescer; + Preconditions.checkArgument(maxEvents >= 1, "maxEvents should be at least 1"); + this.adminExecutor = adminExecutor; + this.onFlush = onFlush; + this.window = window; + this.maxEvents = maxEvents; + } + + /** This must be called on eventExecutor too. */ + public void receive(IncomingT element) { + assert adminExecutor.inEventLoop(); + if (stopped) { + return; + } + if (window.isZero() || maxEvents == 1) { + LOG.debug( + "Received {}, flushing immediately (window = {}, maxEvents = {})", + element, + window, + maxEvents); + onFlush.accept(coalescer.apply(ImmutableList.of(element))); + } else { + currentBatch.add(element); + if (currentBatch.size() == maxEvents) { + LOG.debug( + "Received {}, flushing immediately (because {} accumulated events)", + element, + maxEvents); + flushNow(); + } else { + LOG.debug("Received {}, scheduling next flush in {}", element, window); + scheduleFlush(); + } + } + } + + public void flushNow() { + assert adminExecutor.inEventLoop(); + LOG.debug("Flushing now"); + cancelNextFlush(); + if (!currentBatch.isEmpty()) { + onFlush.accept(coalescer.apply(currentBatch)); + currentBatch = new ArrayList<>(); + } + } + + private void scheduleFlush() { + assert adminExecutor.inEventLoop(); + cancelNextFlush(); + nextFlush = adminExecutor.schedule(this::flushNow, window.toNanos(), TimeUnit.NANOSECONDS); + nextFlush.addListener(UncaughtExceptions::log); + } + + private void cancelNextFlush() { + assert adminExecutor.inEventLoop(); + if (nextFlush != null && !nextFlush.isDone()) { + boolean cancelled = nextFlush.cancel(true); + if (cancelled) { + LOG.debug("Cancelled existing scheduled flush"); + } + } + } + + /** + * Stop debouncing: the next flush is cancelled, and all pending and future events will be + * ignored. + */ + public void stop() { + assert adminExecutor.inEventLoop(); + if (!stopped) { + stopped = true; + cancelNextFlush(); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/LazyReference.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/LazyReference.java new file mode 100644 index 00000000000..099f2c2cc8a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/LazyReference.java @@ -0,0 +1,60 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; +import net.jcip.annotations.ThreadSafe; + +/** Holds a reference to an object that is initialized on first access. */ +@ThreadSafe +public class LazyReference { + + private final String name; + private final Supplier supplier; + private final CycleDetector checker; + private volatile T value; + private ReentrantLock lock = new ReentrantLock(); + + public LazyReference(String name, Supplier supplier, CycleDetector cycleDetector) { + this.name = name; + this.supplier = supplier; + this.checker = cycleDetector; + } + + public T get() { + T t = value; + if (t == null) { + checker.onTryLock(this); + lock.lock(); + try { + checker.onLockAcquired(this); + t = value; + if (t == null) { + value = t = supplier.get(); + } + } finally { + checker.onReleaseLock(this); + lock.unlock(); + } + } + return t; + } + + public String getName() { + return name; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/Reconnection.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/Reconnection.java new file mode 100644 index 00000000000..d40265bd09a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/Reconnection.java @@ -0,0 +1,236 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy.ReconnectionSchedule; +import com.datastax.oss.driver.internal.core.util.Loggers; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ScheduledFuture; +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import net.jcip.annotations.NotThreadSafe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A reconnection process that, if failed, is retried periodically according to the intervals + * defined by a policy. + * + *

All the tasks run on a Netty event executor that is provided at construction time. Clients are + * also expected to call the public methods on that thread. + */ +@NotThreadSafe // must be confined to executor +public class Reconnection { + private static final Logger LOG = LoggerFactory.getLogger(Reconnection.class); + + private enum State { + STOPPED, + SCHEDULED, // next attempt scheduled but not started yet + ATTEMPT_IN_PROGRESS, // current attempt started and not completed yet + STOP_AFTER_CURRENT, // stopped, but we're letting an in-progress attempt finish + ; + } + + private final String logPrefix; + private final EventExecutor executor; + private final Supplier scheduleSupplier; + private final Callable> reconnectionTask; + private final Runnable onStart; + private final Runnable onStop; + + private State state = State.STOPPED; + private ReconnectionSchedule reconnectionSchedule; + private ScheduledFuture> nextAttempt; + + /** + * @param reconnectionTask the actual thing to try on a reconnection, returns if it succeeded or + * not. + */ + public Reconnection( + String logPrefix, + EventExecutor executor, + Supplier scheduleSupplier, + Callable> reconnectionTask, + Runnable onStart, + Runnable onStop) { + this.logPrefix = logPrefix; + this.executor = executor; + this.scheduleSupplier = scheduleSupplier; + this.reconnectionTask = reconnectionTask; + this.onStart = onStart; + this.onStop = onStop; + } + + public Reconnection( + String logPrefix, + EventExecutor executor, + Supplier scheduleSupplier, + Callable> reconnectionTask) { + this(logPrefix, executor, scheduleSupplier, reconnectionTask, () -> {}, () -> {}); + } + + /** + * Note that if {@link #stop()} was called but we're still waiting for the last pending attempt to + * complete, this still returns {@code true}. + */ + public boolean isRunning() { + assert executor.inEventLoop(); + return state != State.STOPPED; + } + + /** This is a no-op if the reconnection is already running. */ + public void start() { + start(null); + } + + public void start(ReconnectionSchedule customSchedule) { + assert executor.inEventLoop(); + switch (state) { + case SCHEDULED: + case ATTEMPT_IN_PROGRESS: + // nothing to do + break; + case STOP_AFTER_CURRENT: + // cancel the scheduled stop + state = State.ATTEMPT_IN_PROGRESS; + break; + case STOPPED: + reconnectionSchedule = (customSchedule == null) ? scheduleSupplier.get() : customSchedule; + onStart.run(); + scheduleNextAttempt(); + break; + } + } + + /** + * Forces a reconnection now, without waiting for the next scheduled attempt. + * + * @param forceIfStopped if true and the reconnection is not running, it will get started (meaning + * subsequent reconnections will be scheduled if this attempt fails). If false and the + * reconnection is not running, no attempt is scheduled. + */ + public void reconnectNow(boolean forceIfStopped) { + assert executor.inEventLoop(); + if (state == State.ATTEMPT_IN_PROGRESS || state == State.STOP_AFTER_CURRENT) { + LOG.debug( + "[{}] reconnectNow and current attempt was still running, letting it complete", + logPrefix); + if (state == State.STOP_AFTER_CURRENT) { + // Make sure that we will schedule other attempts if this one fails. + state = State.ATTEMPT_IN_PROGRESS; + } + } else if (state == State.STOPPED && !forceIfStopped) { + LOG.debug("[{}] reconnectNow(false) while stopped, nothing to do", logPrefix); + } else { + assert state == State.SCHEDULED || (state == State.STOPPED && forceIfStopped); + LOG.debug("[{}] Forcing next attempt now", logPrefix); + if (nextAttempt != null) { + nextAttempt.cancel(true); + } + try { + onNextAttemptStarted(reconnectionTask.call()); + } catch (Exception e) { + Loggers.warnWithException( + LOG, "[{}] Uncaught error while starting reconnection attempt", logPrefix, e); + scheduleNextAttempt(); + } + } + } + + public void stop() { + assert executor.inEventLoop(); + switch (state) { + case STOPPED: + case STOP_AFTER_CURRENT: + break; + case ATTEMPT_IN_PROGRESS: + state = State.STOP_AFTER_CURRENT; + break; + case SCHEDULED: + reallyStop(); + break; + } + } + + private void reallyStop() { + LOG.debug("[{}] Stopping reconnection", logPrefix); + state = State.STOPPED; + if (nextAttempt != null) { + nextAttempt.cancel(true); + nextAttempt = null; + } + onStop.run(); + reconnectionSchedule = null; + } + + private void scheduleNextAttempt() { + assert executor.inEventLoop(); + state = State.SCHEDULED; + if (reconnectionSchedule == null) { // happens if reconnectNow() while we were stopped + reconnectionSchedule = scheduleSupplier.get(); + } + Duration nextInterval = reconnectionSchedule.nextDelay(); + LOG.debug("[{}] Scheduling next reconnection in {}", logPrefix, nextInterval); + nextAttempt = executor.schedule(reconnectionTask, nextInterval.toNanos(), TimeUnit.NANOSECONDS); + nextAttempt.addListener( + (Future> f) -> { + if (f.isSuccess()) { + onNextAttemptStarted(f.getNow()); + } else if (!f.isCancelled()) { + Loggers.warnWithException( + LOG, + "[{}] Uncaught error while starting reconnection attempt", + logPrefix, + f.cause()); + scheduleNextAttempt(); + } + }); + } + + // When the Callable runs this means the caller has started the attempt, we have yet to wait on + // the CompletableFuture to find out if that succeeded or not. + private void onNextAttemptStarted(CompletionStage futureOutcome) { + assert executor.inEventLoop(); + state = State.ATTEMPT_IN_PROGRESS; + futureOutcome + .whenCompleteAsync(this::onNextAttemptCompleted, executor) + .exceptionally(UncaughtExceptions::log); + } + + private void onNextAttemptCompleted(Boolean success, Throwable error) { + assert executor.inEventLoop(); + if (success) { + LOG.debug("[{}] Reconnection successful", logPrefix); + reallyStop(); + } else { + if (error != null && !(error instanceof CancellationException)) { + Loggers.warnWithException( + LOG, "[{}] Uncaught error while starting reconnection attempt", logPrefix, error); + } + if (state == State.STOP_AFTER_CURRENT) { + reallyStop(); + } else { + assert state == State.ATTEMPT_IN_PROGRESS; + scheduleNextAttempt(); + } + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/ReplayingEventFilter.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/ReplayingEventFilter.java new file mode 100644 index 00000000000..5d6fd62918a --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/ReplayingEventFilter.java @@ -0,0 +1,114 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import com.datastax.oss.driver.shaded.guava.common.annotations.VisibleForTesting; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; +import net.jcip.annotations.GuardedBy; +import net.jcip.annotations.ThreadSafe; + +/** + * Filters a list of events, accumulating them during an initialization period. + * + *

It has three states: + * + *

    + *
  • Not started: events are discarded. + *
  • Started: events accumulate but are not propagated to the end consumer yet. + *
  • Ready: all accumulated events are flushed to the end consumer; subsequent events are + * propagated directly. The order of events is preserved at all times. + *
+ */ +@ThreadSafe +public class ReplayingEventFilter { + + private enum State { + NEW, + STARTED, + READY + } + + private final Consumer consumer; + + // Exceptionally, we use a lock: it will rarely be contended, and if so for only a short period. + private final ReadWriteLock stateLock = new ReentrantReadWriteLock(); + + @GuardedBy("stateLock") + private State state; + + @GuardedBy("stateLock") + private List recordedEvents; + + public ReplayingEventFilter(Consumer consumer) { + this.consumer = consumer; + this.state = State.NEW; + this.recordedEvents = new CopyOnWriteArrayList<>(); + } + + public void start() { + stateLock.writeLock().lock(); + try { + state = State.STARTED; + } finally { + stateLock.writeLock().unlock(); + } + } + + public void markReady() { + stateLock.writeLock().lock(); + try { + state = State.READY; + for (EventT event : recordedEvents) { + consumer.accept(event); + } + } finally { + stateLock.writeLock().unlock(); + } + } + + public void accept(EventT event) { + stateLock.readLock().lock(); + try { + switch (state) { + case NEW: + break; + case STARTED: + recordedEvents.add(event); + break; + case READY: + consumer.accept(event); + break; + } + } finally { + stateLock.readLock().unlock(); + } + } + + @VisibleForTesting + public List recordedEvents() { + stateLock.readLock().lock(); + try { + return ImmutableList.copyOf(recordedEvents); + } finally { + stateLock.readLock().unlock(); + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/RunOrSchedule.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/RunOrSchedule.java new file mode 100644 index 00000000000..3bf689a4670 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/RunOrSchedule.java @@ -0,0 +1,94 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; + +/** + * Utility to run a task on a Netty event executor (i.e. thread). If we're already on the executor, + * the task is submitted, otherwise it's scheduled. + * + *

Be careful when using this, always keep in mind that the task might be executed synchronously. + * This can lead to subtle bugs when both the calling code and the callback manipulate a collection: + * + *

{@code
+ * List> futureFoos;
+ *
+ * // Scheduled on eventExecutor:
+ * for (int i = 0; i < count; i++) {
+ *   CompletionStage futureFoo = FooFactory.init();
+ *   futureFoos.add(futureFoo);
+ *   // futureFoo happens to be complete by now, so callback gets executed immediately
+ *   futureFoo.whenComplete(RunOrSchedule.on(eventExecutor, () -> callback(futureFoo)));
+ * }
+ *
+ * private void callback(CompletionStage futureFoo) {
+ *    futureFoos.remove(futureFoo); // ConcurrentModificationException!!!
+ * }
+ * }
+ * + * For that kind of situation, it's better to use {@code futureFoo.whenCompleteAsync(theTask, + * eventExecutor)}, so that the task is always scheduled. + */ +public class RunOrSchedule { + + public static void on(EventExecutor executor, Runnable task) { + if (executor.inEventLoop()) { + task.run(); + } else { + executor.submit(task).addListener(UncaughtExceptions::log); + } + } + + public static Consumer on(EventExecutor executor, Consumer task) { + return (t) -> { + if (executor.inEventLoop()) { + task.accept(t); + } else { + executor.submit(() -> task.accept(t)).addListener(UncaughtExceptions::log); + } + }; + } + + public static CompletionStage on( + EventExecutor executor, Callable> task) { + if (executor.inEventLoop()) { + try { + return task.call(); + } catch (Exception e) { + return CompletableFutures.failedFuture(e); + } + } else { + CompletableFuture result = new CompletableFuture<>(); + executor + .submit(task) + .addListener( + ((Future> f) -> { + if (f.isSuccess()) { + CompletableFutures.completeFrom(f.getNow(), result); + } else { + result.completeExceptionally(f.cause()); + } + })); + return result; + } + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/UncaughtExceptions.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/UncaughtExceptions.java new file mode 100644 index 00000000000..1fc9060b710 --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/concurrent/UncaughtExceptions.java @@ -0,0 +1,58 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import com.datastax.oss.driver.internal.core.util.Loggers; +import io.netty.util.concurrent.Future; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility methods to log unexpected exceptions in asynchronous tasks. + * + *

Use this whenever you execute a future callback to apply side effects, but throw away the + * future itself: + * + *

{@code
+ * CompletionStage futureFoo = FooFactory.build();
+ *
+ * futureFoo
+ *   .whenComplete((f, error) -> { handler code with side effects })
+ *   // futureFoo is not propagated, do this or any unexpected error in the handler will be
+ *   // swallowed
+ *   .exceptionally(UncaughtExceptions::log);
+ *
+ * // If you return the future, you don't need it (but it's up to the caller to handle a failed
+ * // future)
+ * return futureFoo.whenComplete(...)
+ * }
+ */ +public class UncaughtExceptions { + + private static final Logger LOG = LoggerFactory.getLogger(UncaughtExceptions.class); + + public static void log(Future future) { + if (!future.isSuccess() && !future.isCancelled()) { + Loggers.warnWithException(LOG, "Uncaught exception in scheduled task", future.cause()); + } + } + + @SuppressWarnings("TypeParameterUnusedInFormals") // type parameter is only needed for chaining + public static T log(Throwable t) { + Loggers.warnWithException(LOG, "Uncaught exception in scheduled task", t); + return null; + } +} diff --git a/core/src/main/java/com/datastax/oss/driver/internal/core/util/package-info.java b/core/src/main/java/com/datastax/oss/driver/internal/core/util/package-info.java new file mode 100644 index 00000000000..17aae22cc7b --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/core/util/package-info.java @@ -0,0 +1,17 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** Internal utilities specific to Netty. */ +package com.datastax.oss.driver.internal.core.util; diff --git a/core/src/main/java/com/datastax/oss/driver/internal/package-info.java b/core/src/main/java/com/datastax/oss/driver/internal/package-info.java new file mode 100644 index 00000000000..1ba376a01dc --- /dev/null +++ b/core/src/main/java/com/datastax/oss/driver/internal/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Internal implementation details of the driver. + * + *

The types present here (and in subpackages) should not be used from client applications. If + * you decide to use them, do so at your own risk: binary compatibility is best-effort, and we + * reserve the right to break things at any time. Documentation may be sparse + */ +package com.datastax.oss.driver.internal; diff --git a/core/src/main/resources/com/datastax/oss/driver/Driver.properties b/core/src/main/resources/com/datastax/oss/driver/Driver.properties new file mode 100644 index 00000000000..a62ba0a538a --- /dev/null +++ b/core/src/main/resources/com/datastax/oss/driver/Driver.properties @@ -0,0 +1,26 @@ +# +# Copyright DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Note: properties files should be encoded in ISO-8859-1, but we keep this one +# encoded in UTF-8 because that's much easier when building with Maven. + +driver.groupId=${project.groupId} +driver.artifactId=${project.artifactId} +driver.version=${project.version} +# It would be better to use ${project.parent.name} here, but for some reason the bundle plugin +# prevents that from being resolved correctly (unlike the project-level properties above). +# The value is not likely to change, so we simply hard-code it: +driver.name=DataStax Java driver for Apache Cassandra(R) diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf new file mode 100644 index 00000000000..2620963bd80 --- /dev/null +++ b/core/src/main/resources/reference.conf @@ -0,0 +1,1452 @@ +# Reference configuration for the DataStax Java driver for Apache Cassandra®. +# +# Unless you use a custom mechanism to load your configuration (see +# SessionBuilder.withConfigLoader), all the values declared here will be used as defaults. You can +# place your own `application.conf` in the classpath to override them. +# +# Options are classified into two categories: +# - basic: what is most likely to be customized first when kickstarting a new application. +# - advanced: more elaborate tuning options, or "expert"-level customizations. +# +# This file is in HOCON format, see https://github.com/typesafehub/config/blob/master/HOCON.md. +datastax-java-driver { + + # BASIC OPTIONS ---------------------------------------------------------------------------------- + + # The contact points to use for the initial connection to the cluster. + # + # These are addresses of Cassandra nodes that the driver uses to discover the cluster topology. + # Only one contact point is required (the driver will retrieve the address of the other nodes + # automatically), but it is usually a good idea to provide more than one contact point, because if + # that single contact point is unavailable, the driver cannot initialize itself correctly. + # + # This must be a list of strings with each contact point specified as "host:port". If the host is + # a DNS name that resolves to multiple A-records, all the corresponding addresses will be used. Do + # not use "localhost" as the host name (since it resolves to both IPv4 and IPv6 addresses on some + # platforms). + # + # Note that Cassandra 3 and below requires all nodes in a cluster to share the same port (see + # CASSANDRA-7544). + # + # Contact points can also be provided programmatically when you build a cluster instance. If both + # are specified, they will be merged. If both are absent, the driver will default to + # 127.0.0.1:9042. + # + # Required: no + # Modifiable at runtime: no + # Overridable in a profile: no + // basic.contact-points = [ "127.0.0.1:9042", "127.0.0.2:9042" ] + + # A name that uniquely identifies the driver instance created from this configuration. This is + # used as a prefix for log messages and metrics. + # + # If this option is absent, the driver will generate an identifier composed of the letter 's' + # followed by an incrementing counter. If you provide a different value, try to keep it short to + # keep the logs readable. Also, make sure it is unique: reusing the same value will not break the + # driver, but it will mix up the logs and metrics. + # + # Required: no + # Modifiable at runtime: no + # Overridable in a profile: no + // basic.session-name = my_session + + # The name of the keyspace that the session should initially be connected to. + # + # This expects the same format as in a CQL query: case-sensitive names must be quoted (note that + # the quotes must be escaped in HOCON format). For example: + # session-keyspace = case_insensitive_name + # session-keyspace = \"CaseSensitiveName\" + # + # If this option is absent, the session won't be connected to any keyspace, and you'll have to + # either qualify table names in your queries, or use the per-query keyspace feature available in + # Cassandra 4 and above (see Request.getKeyspace()). + # + # This can also be provided programatically in CqlSessionBuilder. + # + # Required: no + # Modifiable at runtime: no + # Overridable in a profile: no + // basic.session-keyspace = my_keyspace + + # How often the driver tries to reload the configuration. + # + # To disable periodic reloading, set this to 0. + # + # Required: yes (unless you pass a different ConfigLoader to the session builder). + # Modifiable at runtime: yes, the new value will be used after the next time the configuration + # gets reloaded. + # Overridable in a profile: no + basic.config-reload-interval = 5 minutes + + basic.request { + # How long the driver waits for a request to complete. This is a global limit on the duration of + # a session.execute() call, including any internal retries the driver might do. + # + # By default, this value is set pretty high to ensure that DDL queries don't time out, in order + # to provide the best experience for new users trying the driver with the out-of-the-box + # configuration. + # For any serious deployment, we recommend that you use separate configuration profiles for DDL + # and DML; you can then set the DML timeout much lower (down to a few milliseconds if needed). + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for requests issued after the change. + # Overridable in a profile: yes + timeout = 2 seconds + + # The consistency level. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for requests issued after the change. + # Overridable in a profile: yes + consistency = LOCAL_ONE + + # The page size. This controls how many rows will be retrieved simultaneously in a single + # network roundtrip (the goal being to avoid loading too many results in memory at the same + # time). If there are more results, additional requests will be used to retrieve them (either + # automatically if you iterate with the sync API, or explicitly with the async API's + # fetchNextPage method). + # If the value is 0 or negative, it will be ignored and the request will not be paged. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for requests issued after the change. + # Overridable in a profile: yes + page-size = 5000 + + # The serial consistency level. + # The allowed values are SERIAL and LOCAL_SERIAL. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for requests issued after the change. + # Overridable in a profile: yes + serial-consistency = SERIAL + + # The default idempotence of a request, that will be used for all `Request` instances where + # `isIdempotent()` returns null. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for requests issued after the change. + # Overridable in a profile: yes + default-idempotence = false + } + + # The policy that decides the "query plan" for each query; that is, which nodes to try as + # coordinators, and in which order. + # + # Required: yes + # Modifiable at runtime: no (but custom implementations may elect to watch configuration changes + # and allow child options to be changed at runtime). + # Overridable in a profile: yes. Note that the driver creates as few instances as possible: if a + # named profile inherits from the default profile, or if two sibling profiles have the exact + # same configuration, they will share a single policy instance at runtime. + # If there are multiple load balancing policies in a single driver instance, they work together + # in the following way: + # - each request gets a query plan from its profile's policy (or the default policy if the + # request has no profile, or the profile does not override the policy). + # - when the policies assign distances to nodes, the driver uses the closest assigned distance + # for any given node. + basic.load-balancing-policy { + # The class of the policy. If it is not qualified, the driver assumes that it resides in the + # package com.datastax.oss.driver.internal.core.loadbalancing. + # + # The driver provides a single implementation out of the box: DefaultLoadBalancingPolicy. + # + # You can also specify a custom class that implements LoadBalancingPolicy and has a public + # constructor with two arguments: the DriverContext and a String representing the profile name. + class = DefaultLoadBalancingPolicy + + # The datacenter that is considered "local": the default policy will only include nodes from + # this datacenter in its query plans. + # + # This option can only be absent if you specified no contact points: in that case, the driver + # defaults to 127.0.0.1:9042, and that node's datacenter is used as the local datacenter. + # + # As soon as you provide contact points (either through the configuration or through the cluster + # builder), you must define the local datacenter explicitly, and initialization will fail if + # this property is absent. In addition, all contact points should be from this datacenter; + # warnings will be logged for nodes that are from a different one. + # + # This can also be specified programmatically with SessionBuilder.withLocalDatacenter. If both + # are specified, the programmatic value takes precedence. + // local-datacenter = datacenter1 + + # A custom filter to include/exclude nodes. + # + # This option is not required; if present, it must be the fully-qualified name of a class that + # implements `java.util.function.Predicate`, and has a public constructor taking a single + # `DriverContext` argument. + # + # Alternatively, you can pass an instance of your filter to + # CqlSession.builder().withNodeFilter(). In that case, this option will be ignored. + # + # The predicate's `test(Node)` method will be invoked each time the policy processes a + # topology or state change: if it returns false, the node will be set at distance IGNORED + # (meaning the driver won't ever connect to it), and never included in any query plan. + // filter.class= + } + + + # ADVANCED OPTIONS ------------------------------------------------------------------------------- + + advanced.connection { + # The timeout to use for internal queries that run as part of the initialization process, just + # after we open a connection. If this timeout fires, the initialization of the connection will + # fail. If this is the first connection ever, the driver will fail to initialize as well, + # otherwise it will retry the connection later. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + init-query-timeout = 500 milliseconds + + # The timeout to use when the driver changes the keyspace on a connection at runtime (this + # happens when the client issues a `USE ...` query, and all connections belonging to the current + # session need to be updated). + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + set-keyspace-timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} + + # The driver maintains a connection pool to each node, according to the distance assigned to it + # by the load balancing policy. If the distance is IGNORED, no connections are maintained. + pool { + local { + # The number of connections in the pool. + # + # Required: yes + # Modifiable at runtime: yes; when the change is detected, all active pools will be notified + # and will adjust their size. + # Overridable in a profile: no + size = 1 + } + remote { + size = 1 + } + } + + # The maximum number of requests that can be executed concurrently on a connection. This must be + # between 1 and 32768. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + max-requests-per-connection = 1024 + + # The maximum number of "orphaned" requests before a connection gets closed automatically. + # + # Sometimes the driver writes to a node but stops listening for a response (for example if the + # request timed out, or was completed by another node). But we can't safely reuse the stream id + # on this connection until we know for sure that the server is done with it. Therefore the id is + # marked as "orphaned" until we get a response from the node. + # + # If the response never comes (or is lost because of a network issue), orphaned ids can + # accumulate over time, eventually affecting the connection's throughput. So we monitor them + # and close the connection above a given threshold (the pool will replace it). + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + max-orphan-requests = 24576 + + # Whether to log non-fatal errors when the driver tries to open a new connection. + # + # This error as recoverable, as the driver will try to reconnect according to the reconnection + # policy. Therefore some users see them as unnecessary clutter in the logs. On the other hand, + # those logs can be handy to debug a misbehaving node. + # + # Note that some type of errors are always logged, regardless of this option: + # - protocol version mismatches (the node gets forced down) + # - when the cluster name in system.local doesn't match the other nodes (the node gets forced + # down) + # - authentication errors (will be retried) + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + warn-on-init-error = true + } + + # Whether to schedule reconnection attempts if all contact points are unreachable on the first + # initialization attempt. + # + # If this is true, the driver will retry according to the reconnection policy. The + # `SessionBuilder.build()` call -- or the future returned by `SessionBuilder.buildAsync()` -- + # won't complete until a contact point has been reached. + # + # If this is false and no contact points are available, the driver will fail with an + # AllNodesFailedException. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + advanced.reconnect-on-init = false + + # The policy that controls how often the driver tries to re-establish connections to down nodes. + # + # Required: yes + # Modifiable at runtime: no (but custom implementations may elect to watch configuration changes + # and allow child options to be changed at runtime). + # Overridable in a profile: no + advanced.reconnection-policy { + # The class of the policy. If it is not qualified, the driver assumes that it resides in the + # package com.datastax.oss.driver.internal.core.connection. + # + # The driver provides two implementations out of the box: ExponentialReconnectionPolicy and + # ConstantReconnectionPolicy. + # + # You can also specify a custom class that implements ReconnectionPolicy and has a public + # constructor with a DriverContext argument. + class = ExponentialReconnectionPolicy + + # ExponentialReconnectionPolicy starts with the base delay, and doubles it after each failed + # reconnection attempt, up to the maximum delay (after that it stays constant). + # + # ConstantReconnectionPolicy only uses the base-delay value, the interval never changes. + base-delay = 1 second + max-delay = 60 seconds + } + + # The policy that controls if the driver retries requests that have failed on one node. + # + # Required: yes + # Modifiable at runtime: no (but custom implementations may elect to watch configuration changes + # and allow child options to be changed at runtime). + # Overridable in a profile: yes. Note that the driver creates as few instances as possible: if a + # named profile inherits from the default profile, or if two sibling profiles have the exact + # same configuration, they will share a single policy instance at runtime. + advanced.retry-policy { + # The class of the policy. If it is not qualified, the driver assumes that it resides in the + # package com.datastax.oss.driver.internal.core.retry. + # + # The driver provides a single implementation out of the box: DefaultRetryPolicy. + # + # You can also specify a custom class that implements RetryPolicy and has a public constructor + # with two arguments: the DriverContext and a String representing the profile name. + class = DefaultRetryPolicy + } + + # The policy that controls if the driver pre-emptively tries other nodes if a node takes too long + # to respond. + # + # Required: yes + # Modifiable at runtime: no (but custom implementations may elect to watch configuration changes + # and allow child options to be changed at runtime). + # Overridable in a profile: yes. Note that the driver creates as few instances as possible: if a + # named profile inherits from the default profile, or if two sibling profiles have the exact + # same configuration, they will share a single policy instance at runtime. + advanced.speculative-execution-policy { + # The class of the policy. If it is not qualified, the driver assumes that it resides in the + # package com.datastax.oss.driver.internal.core.specex. + # + # The following implementations are available out of the box: + # - NoSpeculativeExecutionPolicy: never schedule any speculative execution + # - ConstantSpeculativeExecutionPolicy: schedule executions based on constant delays. This + # requires the `max-executions` and `delay` options below. + # + # You can also specify a custom class that implements SpeculativeExecutionPolicy and has a + # public constructor with two arguments: the DriverContext and a String representing the + # profile name. + class = NoSpeculativeExecutionPolicy + + # The maximum number of executions (including the initial, non-speculative execution). + # This must be at least one. + // max-executions = 3 + + # The delay between each execution. 0 is allowed, and will result in all executions being sent + # simultaneously when the request starts. + # Note that sub-millisecond precision is not supported, any excess precision information will be + # dropped; in particular, delays of less than 1 millisecond are equivalent to 0. + # This must be positive or 0. + // delay = 100 milliseconds + } + + # The component that handles authentication on each new connection. + # + # Required: no. If the 'class' child option is absent, no authentication will occur. + # Modifiable at runtime: no + # Overridable in a profile: no + advanced.auth-provider { + # The class of the provider. If it is not qualified, the driver assumes that it resides in the + # package com.datastax.oss.driver.internal.core.auth. + # + # The driver provides a single implementation out of the box: PlainTextAuthProvider, that uses + # plain-text credentials. It requires the `username` and `password` options below. + # + # You can also specify a custom class that implements AuthProvider and has a public + # constructor with a DriverContext argument. + // class = PlainTextAuthProvider + + # Sample configuration for the plain-text provider: + // username = cassandra + // password = cassandra + } + + # The SSL engine factory that will initialize an SSL engine for each new connection to a server. + # + # Required: no. If the 'class' child option is absent, SSL won't be activated. + # Modifiable at runtime: no + # Overridable in a profile: no + advanced.ssl-engine-factory { + # The class of the factory. If it is not qualified, the driver assumes that it resides in the + # package com.datastax.oss.driver.internal.core.ssl. + # + # The driver provides a single implementation out of the box: DefaultSslEngineFactory, that uses + # the JDK's built-in SSL implementation. + # + # You can also specify a custom class that implements SslEngineFactory and has a public + # constructor with a DriverContext argument. + // class = DefaultSslEngineFactory + + # Sample configuration for the default SSL factory: + # The cipher suites to enable when creating an SSLEngine for a connection. + # This property is optional. If it is not present, the driver won't explicitly enable cipher + # suites on the engine, which according to the JDK documentations results in "a minimum quality + # of service". + // cipher-suites = [ "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA" ] + + # Whether or not to require validation that the hostname of the server certificate's common + # name matches the hostname of the server being connected to. If not set, defaults to true. + // hostname-validation = true + + # The locations and passwords used to access truststore and keystore contents. + # These properties are optional. If either truststore-path or keystore-path are specified, + # the driver builds an SSLContext from these files. If neither option is specified, the + # default SSLContext is used, which is based on system property configuration. + // truststore-path = /path/to/client.truststore + // truststore-password = password123 + // keystore-path = /path/to/client.keystore + // keystore-password = password123 + } + + # The generator that assigns a microsecond timestamp to each request. + # + # Required: yes + # Modifiable at runtime: no (but custom implementations may elect to watch configuration changes + # and allow child options to be changed at runtime). + # Overridable in a profile: yes. Note that the driver creates as few instances as possible: if a + # named profile inherits from the default profile, or if two sibling profiles have the exact + # same configuration, they will share a single generator instance at runtime. + advanced.timestamp-generator { + # The class of the generator. If it is not qualified, the driver assumes that it resides in the + # package com.datastax.oss.driver.internal.core.time. + # + # The driver provides the following implementations out of the box: + # - AtomicTimestampGenerator: timestamps are guaranteed to be unique across all client threads. + # - ThreadLocalTimestampGenerator: timestamps that are guaranteed to be unique within each + # thread only. + # - ServerSideTimestampGenerator: do not generate timestamps, let the server assign them. + # + # You can also specify a custom class that implements TimestampGenerator and has a public + # constructor with two arguments: the DriverContext and a String representing the profile name. + class = AtomicTimestampGenerator + + # To guarantee that queries are applied on the server in the same order as the client issued + # them, timestamps must be strictly increasing. But this means that, if the driver sends more + # than one query per microsecond, timestamps will drift in the future. While this could happen + # occasionally under high load, it should not be a regular occurrence. Therefore the built-in + # implementations log a warning to detect potential issues. + drift-warning { + # How far in the future timestamps are allowed to drift before the warning is logged. + # If it is undefined or set to 0, warnings are disabled. + threshold = 1 second + + # How often the warning will be logged if timestamps keep drifting above the threshold. + interval = 10 seconds + } + + # Whether to force the driver to use Java's millisecond-precision system clock. + # If this is false, the driver will try to access the microsecond-precision OS clock via native + # calls (and fallback to the Java one if the native calls fail). + # Unless you explicitly want to avoid native calls, there's no reason to change this. + force-java-clock = false + } + + # A session-wide component that tracks the outcome of requests. + # + # Required: yes + # Modifiable at runtime: no (but custom implementations may elect to watch configuration changes + # and allow child options to be changed at runtime). + # Overridable in a profile: no + advanced.request-tracker { + # The class of the tracker. If it is not qualified, the driver assumes that it resides in the + # package com.datastax.oss.driver.internal.core.tracker. + # + # The driver provides the following implementations out of the box: + # - NoopRequestTracker: does nothing. + # - RequestLogger: logs requests (see the parameters below). + # + # You can also specify a custom class that implements RequestTracker and has a public + # constructor with a DriverContext argument. + class = NoopRequestTracker + + # Parameters for RequestLogger. All of them can be overridden in a profile, and changed at + # runtime (the new values will be taken into account for requests logged after the change). + logs { + # Whether to log successful requests. + // success.enabled = true + + slow { + # The threshold to classify a successful request as "slow". If this is unset, all successful + # requests will be considered as normal. + // threshold = 1 second + + # Whether to log slow requests. + // enabled = true + } + + # Whether to log failed requests. + // error.enabled = true + + # The maximum length of the query string in the log message. If it is longer than that, it + # will be truncated. + // max-query-length = 500 + + # Whether to log bound values in addition to the query string. + // show-values = true + + # The maximum length for bound values in the log message. If the formatted representation of a + # value is longer than that, it will be truncated. + // max-value-length = 50 + + # The maximum number of bound values to log. If a request has more values, the list of values + # will be truncated. + // max-values = 50 + + # Whether to log stack traces for failed queries. If this is disabled, the log will just + # include the exception's string representation (generally the class name and message). + // show-stack-traces = true + } + } + + # A session-wide component that controls the rate at which requests are executed. + # + # Implementations vary, but throttlers generally track a metric that represents the level of + # utilization of the session, and prevent new requests from starting when that metric exceeds a + # threshold. Pending requests may be enqueued and retried later. + # + # From the public API's point of view, this process is mostly transparent: any time that the + # request is throttled is included in the session.execute() or session.executeAsync() call. + # Similarly, the request timeout encompasses throttling: the timeout starts ticking before the + # throttler has started processing the request; a request may time out while it is still in the + # throttler's queue, before the driver has even tried to send it to a node. + # + # The only visible effect is that a request may fail with a RequestThrottlingException, if the + # throttler has determined that it can neither allow the request to proceed now, nor enqueue it; + # this indicates that your session is overloaded. + # + # Required: yes + # Modifiable at runtime: no (but custom implementations may elect to watch configuration changes + # and allow child options to be changed at runtime). + # Overridable in a profile: no + advanced.throttler { + # The class of the throttler. If it is not qualified, the driver assumes that it resides in + # the package com.datastax.oss.driver.internal.core.session.throttling. + # + # The driver provides the following implementations out of the box: + # + # - PassThroughRequestThrottler: does not perform any kind of throttling, all requests are + # allowed to proceed immediately. Required options: none. + # + # - ConcurrencyLimitingRequestThrottler: limits the number of requests that can be executed in + # parallel. Required options: max-concurrent-requests, max-queue-size. + # + # - RateLimitingRequestThrottler: limits the request rate per second. Required options: + # max-requests-per-second, max-queue-size, drain-interval. + # + # You can also specify a custom class that implements RequestThrottler and has a public + # constructor with a DriverContext argument. + class = PassThroughRequestThrottler + + # The maximum number of requests that can be enqueued when the throttling threshold is exceeded. + # Beyond that size, requests will fail with a RequestThrottlingException. + // max-queue-size = 10000 + + # The maximum number of requests that are allowed to execute in parallel. + # Only used by ConcurrencyLimitingRequestThrottler. + // max-concurrent-requests = 10000 + + # The maximum allowed request rate. + # Only used by RateLimitingRequestThrottler. + // max-requests-per-second = 10000 + + # How often the throttler attempts to dequeue requests. This is the only way for rate-based + # throttling, because the completion of an active request does not necessarily free a "slot" for + # a queued one (the rate might still be too high). + # + # You want to set this high enough that each attempt will process multiple entries in the queue, + # but not delay requests too much. A few milliseconds is probably a happy medium. + # + # Only used by RateLimitingRequestThrottler. + // drain-interval = 10 milliseconds + } + + # A session-wide component that listens for node state changes. If it is not qualified, the driver + # assumes that it resides in the package com.datastax.oss.driver.internal.core.metadata. + # + # The driver provides a single no-op implementation out of the box: NoopNodeStateListener. + # + # You can also specify a custom class that implements NodeStateListener and has a public + # constructor with a DriverContext argument. + # + # Alternatively, you can pass an instance of your listener programmatically with + # CqlSession.builder().withNodeStateListener(). In that case, this option will be ignored. + # + # Required: unless a listener has been provided programmatically + # Modifiable at runtime: no (but custom implementations may elect to watch configuration changes + # and allow child options to be changed at runtime). + # Overridable in a profile: no + advanced.node-state-listener.class = NoopNodeStateListener + + # A session-wide component that listens for node state changes. If it is not qualified, the driver + # assumes that it resides in the package com.datastax.oss.driver.internal.core.metadata.schema. + # + # The driver provides a single no-op implementation out of the box: NoopSchemaChangeListener. + # + # You can also specify a custom class that implements SchemaChangeListener and has a public + # constructor with a DriverContext argument. + # + # Alternatively, you can pass an instance of your listener programmatically with + # CqlSession.builder().withSchemaChangeListener(). In that case, this option will be ignored. + # + # Required: unless a listener has been provided programmatically + # Modifiable at runtime: no (but custom implementations may elect to watch configuration changes + # and allow child options to be changed at runtime). + # Overridable in a profile: no + advanced.schema-change-listener.class = NoopSchemaChangeListener + + # The address translator to use to convert the addresses sent by Cassandra nodes into ones that + # the driver uses to connect. + # This is only needed if the nodes are not directly reachable from the driver (for example, the + # driver is in a different network region and needs to use a public IP, or it connects through a + # proxy). + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + advanced.address-translator { + # The class of the translator. If it is not qualified, the driver assumes that it resides in + # the package com.datastax.oss.driver.internal.core.addresstranslation. + # + # The driver provides the following implementations out of the box: + # - PassThroughAddressTranslator: returns all addresses unchanged + # + # You can also specify a custom class that implements AddressTranslator and has a public + # constructor with a DriverContext argument. + class = PassThroughAddressTranslator + } + + # Whether to resolve the addresses passed to `basic.contact-points`. + # + # If this is true, addresses are created with `InetSocketAddress(String, int)`: the host name will + # be resolved the first time, and the driver will use the resolved IP address for all subsequent + # connection attempts. + # + # If this is false, addresses are created with `InetSocketAddress.createUnresolved()`: the host + # name will be resolved again every time the driver opens a new connection. This is useful for + # containerized environments where DNS records are more likely to change over time (note that the + # JVM and OS have their own DNS caching mechanisms, so you might need additional configuration + # beyond the driver). + # + # This option only applies to the contact points specified in the configuration. It has no effect + # on: + # - programmatic contact points passed to SessionBuilder.addContactPoints: these addresses are + # built outside of the driver, so it is your responsibility to provide unresolved instances. + # - dynamically discovered peers: the driver relies on Cassandra system tables, which expose raw + # IP addresses. Use a custom address translator to convert them to unresolved addresses (if + # you're in a containerized environment, you probably already need address translation anyway). + # + # Required: no (defaults to true) + # Modifiable at runtime: no + # Overridable in a profile: no + advanced.resolve-contact-points = true + + advanced.protocol { + # The native protocol version to use. + # + # If this option is absent, the driver looks up the versions of the nodes at startup (by default + # in system.peers.release_version), and chooses the highest common protocol version. + # For example, if you have a mixed cluster with Apache Cassandra 2.1 nodes (protocol v3) and + # Apache Cassandra 3.0 nodes (protocol v3 and v4), then protocol v3 is chosen. If the nodes + # don't have a common protocol version, initialization fails. + # + # If this option is set, then the given version will be used for all connections, without any + # negotiation or downgrading. If any of the contact points doesn't support it, that contact + # point will be skipped. + # + # Once the protocol version is set, it can't change for the rest of the driver's lifetime; if + # an incompatible node joins the cluster later, connection will fail and the driver will force + # it down (i.e. never try to connect to it again). + # + # You can check the actual version at runtime with Cluster.getContext().getProtocolVersion(). + # + # Required: no + # Modifiable at runtime: no + # Overridable in a profile: no + // version = V4 + + # The name of the algorithm used to compress protocol frames. + # + # The possible values are: + # - lz4: requires net.jpountz.lz4:lz4 in the classpath. + # - snappy: requires org.xerial.snappy:snappy-java in the classpath. + # + # The driver depends on the compression libraries, but they are optional. Make sure you + # redeclare an explicit dependency in your project. Refer to the driver's POM or manual for the + # exact version. + # + # Required: no. If the option is absent, protocol frames are not compressed. + # Modifiable at runtime: no + # Overridable in a profile: no + // compression = lz4 + + # The maximum length of the frames supported by the driver. Beyond that limit, requests will + # fail with an exception + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + max-frame-length = 256 MB + } + + advanced.request { + # Whether a warning is logged when a request (such as a CQL `USE ...`) changes the active + # keyspace. + # Switching keyspace at runtime is highly discouraged, because it is inherently unsafe (other + # requests expecting the old keyspace might be running concurrently), and may cause statements + # prepared before the change to fail. + # It should only be done in very specific use cases where there is only a single client thread + # executing synchronous queries (such as a cqlsh-like interpreter). In other cases, clients + # should prefix table names in their queries instead. + # + # Note that CASSANDRA-10145 (scheduled for C* 4.0) will introduce a per-request keyspace option + # as a workaround to this issue. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for keyspace switches occurring after + # the change. + # Overridable in a profile: no + warn-if-set-keyspace = true + + # If tracing is enabled for a query, this controls how the trace is fetched. + trace { + # How many times the driver will attempt to fetch the query if it is not ready yet. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for traces fetched after the change. + # Overridable in a profile: yes + attempts = 5 + + # The interval between each attempt. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for traces fetched after the change. + # Overridable in a profile: yes + interval = 3 milliseconds + + # The consistency level to use for trace queries. + # Note that the default replication strategy for the system_traces keyspace is SimpleStrategy + # with RF=2, therefore LOCAL_ONE might not work if the local DC has no replicas for a given + # trace id. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for traces fetched after the change. + # Overridable in a profile: yes + consistency = ONE + } + + # Whether logging of server warnings generated during query execution should be disabled by the + # driver. All server generated warnings will be available programmatically via the ExecutionInfo + # object on the executed statement's ResultSet. If set to "false", this will prevent the driver + # from logging these warnings. + # + # NOTE: The log formatting for these warning messages will reuse the options defined for + # advanced.request-tracker. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for query warnings received after the change. + # Overridable in a profile: yes + log-warnings = true + } + + advanced.metrics { + # The session-level metrics (all disabled by default). + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + session { + enabled = [ + # The number and rate of bytes sent for the entire session (exposed as a Meter). + // bytes-sent, + + # The number and rate of bytes received for the entire session (exposed as a Meter). + // bytes-received + + # The number of nodes to which the driver has at least one active connection (exposed as a + # Gauge). + // connected-nodes, + + # The throughput and latency percentiles of CQL requests (exposed as a Timer). + # + # This corresponds to the overall duration of the session.execute() call, including any + # retry. + // cql-requests, + + # The number of CQL requests that timed out -- that is, the session.execute() call failed + # with a DriverTimeoutException (exposed as a Counter). + // cql-client-timeouts, + + # The size of the driver-side cache of CQL prepared statements. + # + # The cache uses weak values eviction, so this represents the number of PreparedStatement + # instances that your application has created, and is still holding a reference to. Note + # that the returned value is approximate. + // cql-prepared-cache-size, + + # How long requests are being throttled (exposed as a Timer). + # + # This is the time between the start of the session.execute() call, and the moment when + # the throttler allows the request to proceed. + // throttling.delay, + + # The size of the throttling queue (exposed as a Gauge). + # + # This is the number of requests that the throttler is currently delaying in order to + # preserve its SLA. This metric only works with the built-in concurrency- and rate-based + # throttlers; in other cases, it will always be 0. + // throttling.queue-size, + + # The number of times a request was rejected with a RequestThrottlingException (exposed as + # a Counter) + // throttling.errors, + ] + + # Extra configuration (for the metrics that need it) + + # Required: if the 'cql-requests' metric is enabled + # Modifiable at runtime: no + # Overridable in a profile: no + cql-requests { + # The largest latency that we expect to record. + # + # This should be slightly higher than request.timeout (in theory, readings can't be higher + # than the timeout, but there might be a small overhead due to internal scheduling). + # + # This is used to scale internal data structures. If a higher recording is encountered at + # runtime, it is discarded and a warning is logged. + highest-latency = 3 seconds + + # The number of significant decimal digits to which internal structures will maintain + # value resolution and separation (for example, 3 means that recordings up to 1 second + # will be recorded with a resolution of 1 millisecond or better). + # + # This must be between 0 and 5. If the value is out of range, it defaults to 3 and a + # warning is logged. + significant-digits = 3 + + # The interval at which percentile data is refreshed. + # + # The driver records latency data in a "live" histogram, and serves results from a cached + # snapshot. Each time the snapshot gets older than the interval, the two are switched. + # Note that this switch happens upon fetching the metrics, so if you never fetch the + # recording interval might grow higher (that shouldn't be an issue in a production + # environment because you would typically have a metrics reporter that exports to a + # monitoring tool at a regular interval). + # + # In practice, this means that if you set this to 5 minutes, you're looking at data from a + # 5-minute interval in the past, that is at most 5 minutes old. If you fetch the metrics + # at a faster pace, you will observe the same data for 5 minutes until the interval + # expires. + # + # Note that this does not apply to the total count and rates (those are updated in real + # time). + refresh-interval = 5 minutes + } + + # Required: if the 'throttling.delay' metric is enabled + # Modifiable at runtime: no + # Overridable in a profile: no + throttling.delay { + highest-latency = 3 seconds + significant-digits = 3 + refresh-interval = 5 minutes + } + } + # The node-level metrics (all disabled by default). + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + node { + enabled = [ + # The number of connections open to this node for regular requests (exposed as a + # Gauge). + # + # This includes the control connection (which uses at most one extra connection to a + # random node in the cluster). + // pool.open-connections, + + # The number of stream ids available on the connections to this node (exposed as a + # Gauge). + # + # Stream ids are used to multiplex requests on each connection, so this is an indication + # of how many more requests the node could handle concurrently before becoming saturated + # (note that this is a driver-side only consideration, there might be other limitations on + # the server that prevent reaching that theoretical limit). + // pool.available-streams, + + # The number of requests currently executing on the connections to this node (exposed as a + # Gauge). This includes orphaned streams. + // pool.in-flight, + + # The number of "orphaned" stream ids on the connections to this node (exposed as a + # Gauge). + # + # See the description of the connection.max-orphan-requests option for more details. + // pool.orphaned-streams, + + # The number and rate of bytes sent to this node (exposed as a Meter). + // bytes-sent, + + # The number and rate of bytes received from this node (exposed as a Meter). + // bytes-received, + + # The throughput and latency percentiles of individual CQL messages sent to this node as + # part of an overall request (exposed as a Timer). + # + # Note that this does not necessarily correspond to the overall duration of the + # session.execute() call, since the driver might query multiple nodes because of retries + # and speculative executions. Therefore a single "request" (as seen from a client of the + # driver) can be composed of more than one of the "messages" measured by this metric. + # + # Therefore this metric is intended as an insight into the performance of this particular + # node. For statistics on overall request completion, use the session-level cql-requests. + // cql-messages, + + # The number of times the driver failed to send a request to this node (exposed as a + # Counter). + # + # In those case we know the request didn't even reach the coordinator, so they are retried + # on the next node automatically (without going through the retry policy). + // errors.request.unsent, + + # The number of times a request was aborted before the driver even received a response + # from this node (exposed as a Counter). + # + # This can happen in two cases: if the connection was closed due to an external event + # (such as a network error or heartbeat failure); or if there was an unexpected error + # while decoding the response (this can only be a driver bug). + // errors.request.aborted, + + # The number of times this node replied with a WRITE_TIMEOUT error (exposed as a Counter). + # + # Whether this error is rethrown directly to the client, rethrown or ignored is determined + # by the RetryPolicy. + // errors.request.write-timeouts, + + # The number of times this node replied with a READ_TIMEOUT error (exposed as a Counter). + # + # Whether this error is rethrown directly to the client, rethrown or ignored is determined + # by the RetryPolicy. + // errors.request.read-timeouts, + + # The number of times this node replied with an UNAVAILABLE error (exposed as a Counter). + # + # Whether this error is rethrown directly to the client, rethrown or ignored is determined + # by the RetryPolicy. + // errors.request.unavailables, + + # The number of times this node replied with an error that doesn't fall under other + # 'errors.*' metrics (exposed as a Counter). + // errors.request.others, + + # The total number of errors on this node that caused the RetryPolicy to trigger a retry + # (exposed as a Counter). + # + # This is a sum of all the other retries.* metrics. + // retries.total, + + # The number of errors on this node that caused the RetryPolicy to trigger a retry, broken + # down by error type (exposed as Counters). + // retries.aborted, + // retries.read-timeout, + // retries.write-timeout, + // retries.unavailable, + // retries.other, + + # The total number of errors on this node that were ignored by the RetryPolicy (exposed as + # a Counter). + # + # This is a sum of all the other ignores.* metrics. + // ignores.total, + + # The number of errors on this node that were ignored by the RetryPolicy, broken down by + # error type (exposed as Counters). + // ignores.aborted, + // ignores.read-timeout, + // ignores.write-timeout, + // ignores.unavailable, + // ignores.other, + + # The number of speculative executions triggered by a slow response from this node + # (exposed as a Counter). + // speculative-executions, + + # The number of errors encountered while trying to establish a connection to this node + # (exposed as a Counter). + # + # Connection errors are not a fatal issue for the driver, failed connections will be + # retried periodically according to the reconnection policy. You can choose whether or not + # to log those errors at WARN level with the connection.warn-on-init-error option. + # + # Authentication errors are not included in this counter, they are tracked separately in + # errors.connection.auth. + // errors.connection.init, + + # The number of authentication errors encountered while trying to establish a connection + # to this node (exposed as a Counter). + # Authentication errors are also logged at WARN level. + // errors.connection.auth, + ] + + # See cql-requests in the `session` section + # + # Required: if the 'cql-messages' metric is enabled + # Modifiable at runtime: no + # Overridable in a profile: no + cql-messages { + highest-latency = 3 seconds + significant-digits = 3 + refresh-interval = 5 minutes + } + } + } + + advanced.socket { + # Whether or not to disable the Nagle algorithm. + # + # By default, this option is set to true (Nagle disabled), because the driver has its own + # internal message coalescing algorithm. + # + # See java.net.StandardSocketOptions.TCP_NODELAY. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + tcp-no-delay = true + + # All other socket options are unset by default. The actual value depends on the underlying + # Netty transport: + # - NIO uses the defaults from java.net.Socket (refer to the javadocs of + # java.net.StandardSocketOptions for each option). + # - Epoll delegates to the underlying file descriptor, which uses the O/S defaults. + + # Whether or not to enable TCP keep-alive probes. + # + # See java.net.StandardSocketOptions.SO_KEEPALIVE. + # + # Required: no + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + //keep-alive = false + + # Whether or not to allow address reuse. + # + # See java.net.StandardSocketOptions.SO_REUSEADDR. + # + # Required: no + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + //reuse-address = true + + # Sets the linger interval. + # + # If the value is zero or greater, then it represents a timeout value, in seconds; + # if the value is negative, it means that this option is disabled. + # + # See java.net.StandardSocketOptions.SO_LINGER. + # + # Required: no + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + //linger-interval = 0 + + # Sets a hint to the size of the underlying buffers for incoming network I/O. + # + # See java.net.StandardSocketOptions.SO_RCVBUF. + # + # Required: no + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + //receive-buffer-size = 65535 + + # Sets a hint to the size of the underlying buffers for outgoing network I/O. + # + # See java.net.StandardSocketOptions.SO_SNDBUF. + # + # Required: no + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + //send-buffer-size = 65535 + } + + advanced.heartbeat { + # The heartbeat interval. If a connection stays idle for that duration (no reads), the driver + # sends a dummy message on it to make sure it's still alive. If not, the connection is trashed + # and replaced. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + interval = 30 seconds + + # How long the driver waits for the response to a heartbeat. If this timeout fires, the + # heartbeat is considered failed. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for connections created after the + # change. + # Overridable in a profile: no + timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} + } + + advanced.metadata { + # Topology events are external signals that inform the driver of the state of Cassandra nodes + # (by default, they correspond to gossip events received on the control connection). + # The debouncer helps smoothen out oscillations if conflicting events are sent out in short + # bursts. + # Debouncing may be disabled by setting the window to 0 or max-events to 1 (this is not + # recommended). + topology-event-debouncer { + # How long the driver waits to propagate an event. If another event is received within that + # time, the window is reset and a batch of accumulated events will be delivered. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + window = 1 second + + # The maximum number of events that can accumulate. If this count is reached, the events are + # delivered immediately and the time window is reset. This avoids holding events indefinitely + # if the window keeps getting reset. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + max-events = 20 + } + + # Options relating to schema metadata (Cluster.getMetadata.getKeyspaces). + # This metadata is exposed by the driver for informational purposes, and is also necessary for + # token-aware routing. + schema { + # Whether schema metadata is enabled. + # If this is false, the schema will remain empty, or to the last known value. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for refreshes issued after the + # change. It can also be overridden programmatically via Cluster.setSchemaMetadataEnabled. + # Overridable in a profile: no + enabled = true + + # The list of keyspaces for which schema and token metadata should be maintained. If this + # property is absent or empty, all existing keyspaces are processed. + # + # Required: no + # Modifiable at runtime: yes, the new value will be used for refreshes issued after the + # change. + # Overridable in a profile: no + // refreshed-keyspaces = [ "ks1", "ks2" ] + + # The timeout for the requests to the schema tables. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for refreshes issued after the + # change. + # Overridable in a profile: no + request-timeout = ${datastax-java-driver.basic.request.timeout} + + # The page size for the requests to the schema tables. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for refreshes issued after the + # change. + # Overridable in a profile: no + request-page-size = ${datastax-java-driver.basic.request.page-size} + + # Protects against bursts of schema updates (for example when a client issues a sequence of + # DDL queries), by coalescing them into a single update. + # Debouncing may be disabled by setting the window to 0 or max-events to 1 (this is highly + # discouraged for schema refreshes). + debouncer { + # How long the driver waits to apply a refresh. If another refresh is requested within that + # time, the window is reset and a single refresh will be triggered when it ends. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + window = 1 second + + # The maximum number of refreshes that can accumulate. If this count is reached, a refresh + # is done immediately and the window is reset. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + max-events = 20 + } + } + + # Whether token metadata (Cluster.getMetadata.getTokenMap) is enabled. + # This metadata is exposed by the driver for informational purposes, and is also necessary for + # token-aware routing. + # If this is false, it will remain empty, or to the last known value. Note that its computation + # requires information about the schema; therefore if schema metadata is disabled or filtered to + # a subset of keyspaces, the token map will be incomplete, regardless of the value of this + # property. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for refreshes issued after the change. + # Overridable in a profile: no + token-map.enabled = true + } + + advanced.control-connection { + # How long the driver waits for responses to control queries (e.g. fetching the list of nodes, + # refreshing the schema). + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} + + # Due to the distributed nature of Cassandra, schema changes made on one node might not be + # immediately visible to others. Under certain circumstances, the driver waits until all nodes + # agree on a common schema version (namely: before a schema refresh, before repreparing all + # queries on a newly up node, and before completing a successful schema-altering query). To do + # so, it queries system tables to find out the schema version of all nodes that are currently + # UP. If all the versions match, the check succeeds, otherwise it is retried periodically, until + # a given timeout. + # + # A schema agreement failure is not fatal, but it might produce unexpected results (for example, + # getting an "unconfigured table" error for a table that you created right before, just because + # the two queries went to different coordinators). + # + # Note that schema agreement never succeeds in a mixed-version cluster (it would be challenging + # because the way the schema version is computed varies across server versions); the assumption + # is that schema updates are unlikely to happen during a rolling upgrade anyway. + schema-agreement { + # The interval between each attempt. + # Required: yes + # Modifiable at runtime: yes, the new value will be used for checks issued after the change. + # Overridable in a profile: no + interval = 200 milliseconds + + # The timeout after which schema agreement fails. + # If this is set to 0, schema agreement is skipped and will always fail. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for checks issued after the change. + # Overridable in a profile: no + timeout = 10 seconds + + # Whether to log a warning if schema agreement fails. + # You might want to change this if you've set the timeout to 0. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for checks issued after the change. + # Overridable in a profile: no + warn-on-failure = true + } + } + + advanced.prepared-statements { + # Whether `Session.prepare` calls should be sent to all nodes in the cluster. + # + # A request to prepare is handled in two steps: + # 1) send to a single node first (to rule out simple errors like malformed queries). + # 2) if step 1 succeeds, re-send to all other active nodes (i.e. not ignored by the load + # balancing policy). + # This option controls whether step 2 is executed. + # + # The reason why you might want to disable it is to optimize network usage if you have a large + # number of clients preparing the same set of statements at startup. If your load balancing + # policy distributes queries randomly, each client will pick a different host to prepare its + # statements, and on the whole each host has a good chance of having been hit by at least one + # client for each statement. + # On the other hand, if that assumption turns out to be wrong and one host hasn't prepared a + # given statement, it needs to be re-prepared on the fly the first time it gets executed; this + # causes a performance penalty (one extra roundtrip to resend the query to prepare, and another + # to retry the execution). + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for prepares issued after the change. + # Overridable in a profile: yes + prepare-on-all-nodes = true + + # How the driver replicates prepared statements on a node that just came back up or joined the + # cluster. + reprepare-on-up { + # Whether the driver tries to prepare on new nodes at all. + # + # The reason why you might want to disable it is to optimize reconnection time when you + # believe nodes often get marked down because of temporary network issues, rather than the + # node really crashing. In that case, the node still has prepared statements in its cache when + # the driver reconnects, so re-preparing is redundant. + # + # On the other hand, if that assumption turns out to be wrong and the node had really + # restarted, its prepared statement cache is empty (before CASSANDRA-8831), and statements + # need to be re-prepared on the fly the first time they get executed; this causes a + # performance penalty (one extra roundtrip to resend the query to prepare, and another to + # retry the execution). + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for nodes that come back up after the + # change. + # Overridable in a profile: no + enabled = true + + # Whether to check `system.prepared_statements` on the target node before repreparing. + # + # This table exists since CASSANDRA-8831 (merged in 3.10). It stores the statements already + # prepared on the node, and preserves them across restarts. + # + # Checking the table first avoids repreparing unnecessarily, but the cost of the query is not + # always worth the improvement, especially if the number of statements is low. + # + # If the table does not exist, or the query fails for any other reason, the error is ignored + # and the driver proceeds to reprepare statements according to the other parameters. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for nodes that come back up after the + # change. + # Overridable in a profile: no + check-system-table = false + + # The maximum number of statements that should be reprepared. 0 or a negative value means no + # limit. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for nodes that come back up after the + # change. + # Overridable in a profile: no + max-statements = 0 + + # The maximum number of concurrent requests when repreparing. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for nodes that come back up after the + # change. + # Overridable in a profile: no + max-parallelism = 100 + + # The request timeout. This applies both to querying the system.prepared_statements table (if + # relevant), and the prepare requests themselves. + # + # Required: yes + # Modifiable at runtime: yes, the new value will be used for nodes that come back up after the + # change. + # Overridable in a profile: no + timeout = ${datastax-java-driver.advanced.connection.init-query-timeout} + } + } + + # Options related to the Netty event loop groups used internally by the driver. + advanced.netty { + # The event loop group used for I/O operations (reading and writing to Cassandra nodes). + # By default, threads in this group are named after the session name, "-io-" and an incrementing + # counter, for example "s0-io-0". + io-group { + # The number of threads. + # If this is set to 0, the driver will use `Runtime.getRuntime().availableProcessors() * 2`. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + size = 0 + + # The options to shut down the event loop group gracefully when the driver closes. If a task + # gets submitted during the quiet period, it is accepted and the quiet period starts over. + # The timeout limits the overall shutdown time. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + shutdown {quiet-period = 2, timeout = 15, unit = SECONDS} + } + # The event loop group used for admin tasks not related to request I/O (handle cluster events, + # refresh metadata, schedule reconnections, etc.) + # By default, threads in this group are named after the session name, "-admin-" and an + # incrementing counter, for example "s0-admin-0". + admin-group { + size = 2 + + shutdown {quiet-period = 2, timeout = 15, unit = SECONDS} + } + # The timer used for scheduling request timeouts and speculative executions + # By default, this thread is named after the session name and "-timer-0", for example + # "s0-timer-0". + timer { + # The timer tick duration + # This is the how frequent the timer should wake-up to check for timed out tasks. + # Lower resolution (i.e. longer durations) will leave more CPU cycles for running I/O + # operations at the cost of precision of exactly when a request timeout will expire or a + # speculative execution will run. Higher resolution (i.e. shorter durations) will result in + # more precise request timeouts and speculative execution scheduling, but at the cost of CPU + # cycles taken from I/O operations, which could lead to lower overall I/O throughput. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + tick-duration = 1 millisecond + + # Number of ticks in a Timer wheel. The underlying implementation uses Netty's + # HashedWheelTimer, which uses hashes to arrange the timeouts. This effectively controls the + # size of the timer wheel. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + ticks-per-wheel = 2048 + } + } + + # The component that coalesces writes on the connections. + # This is exposed mainly to facilitate tuning during development. You shouldn't have to adjust + # this. + advanced.coalescer { + # How many times the coalescer is allowed to reschedule itself when it did no work. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + max-runs-with-no-work = 5 + + # The reschedule interval. + # + # Required: yes + # Modifiable at runtime: no + # Overridable in a profile: no + reschedule-interval = 10 microseconds + } + + profiles { + # This is where your custom profiles go, for example: + # olap { + # basic.request.timeout = 5 seconds + # } + } +} \ No newline at end of file diff --git a/core/src/test/java/com/datastax/oss/driver/Assertions.java b/core/src/test/java/com/datastax/oss/driver/Assertions.java new file mode 100644 index 00000000000..6137414a5db --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/Assertions.java @@ -0,0 +1,58 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver; + +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.VersionAssert; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.internal.core.CompletionStageAssert; +import com.datastax.oss.driver.internal.core.DriverConfigAssert; +import com.datastax.oss.driver.internal.core.NettyFutureAssert; +import com.datastax.oss.driver.internal.core.metadata.token.TokenRangeAssert; +import io.netty.buffer.ByteBuf; +import io.netty.util.concurrent.Future; +import java.util.concurrent.CompletionStage; + +public class Assertions extends org.assertj.core.api.Assertions { + public static ByteBufAssert assertThat(ByteBuf actual) { + return new ByteBufAssert(actual); + } + + public static DriverConfigAssert assertThat(DriverConfig actual) { + return new DriverConfigAssert(actual); + } + + public static NettyFutureAssert assertThat(Future actual) { + return new NettyFutureAssert<>(actual); + } + + /** + * Use a different name because this clashes with AssertJ's built-in one. Our implementation is a + * bit more flexible for checking completion values and errors. + */ + public static CompletionStageAssert assertThatStage(CompletionStage actual) { + return new CompletionStageAssert<>(actual); + } + + public static VersionAssert assertThat(Version actual) { + return new VersionAssert(actual); + } + + public static TokenRangeAssert assertThat(TokenRange actual) { + return new TokenRangeAssert(actual); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/ByteBufAssert.java b/core/src/test/java/com/datastax/oss/driver/ByteBufAssert.java new file mode 100644 index 00000000000..917d572e721 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/ByteBufAssert.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.protocol.internal.util.Bytes; +import io.netty.buffer.ByteBuf; +import org.assertj.core.api.AbstractAssert; + +public class ByteBufAssert extends AbstractAssert { + public ByteBufAssert(ByteBuf actual) { + super(actual, ByteBufAssert.class); + } + + public ByteBufAssert containsExactly(String hexString) { + ByteBuf copy = actual.duplicate(); + byte[] expectedBytes = Bytes.fromHexString(hexString).array(); + byte[] actualBytes = new byte[expectedBytes.length]; + copy.readBytes(actualBytes); + assertThat(actualBytes).containsExactly(expectedBytes); + // And nothing more + assertThat(copy.isReadable()).isFalse(); + return this; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/DriverRunListener.java b/core/src/test/java/com/datastax/oss/driver/DriverRunListener.java new file mode 100644 index 00000000000..67c22c56eeb --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/DriverRunListener.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver; + +import static org.assertj.core.api.Assertions.fail; + +import org.junit.runner.Description; +import org.junit.runner.notification.RunListener; + +/** + * Common parent of all driver tests, to store common configuration and perform sanity checks. + * + * @see "maven-surefire-plugin configuration in pom.xml" + */ +public class DriverRunListener extends RunListener { + + @Override + public void testFinished(Description description) throws Exception { + // If a test interrupted the main thread silently, this can make later tests fail. Instead, we + // fail the test and clear the interrupt status. + // Note: Thread.interrupted() also clears the flag, which is what we want. + if (Thread.interrupted()) { + fail(description.getMethodName() + " interrupted the main thread"); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/TestDataProviders.java b/core/src/test/java/com/datastax/oss/driver/TestDataProviders.java new file mode 100644 index 00000000000..866a4bc9f75 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/TestDataProviders.java @@ -0,0 +1,86 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver; + +import com.tngtech.java.junit.dataprovider.DataProvider; +import java.util.Arrays; + +public class TestDataProviders { + + public static Object[][] fromList(Object... l) { + Object[][] result = new Object[l.length][]; + for (int i = 0; i < l.length; i++) { + result[i] = new Object[1]; + result[i][0] = l[i]; + } + return result; + } + + public static Object[][] concat(Object[][] left, Object[][] right) { + Object[][] result = Arrays.copyOf(left, left.length + right.length); + System.arraycopy(right, 0, result, left.length, right.length); + return result; + } + + // example: [ [a,b], [c,d] ], [ [1], [2] ], [ [true], [false] ] + // => [ [a,b,1,true], [a,b,1,false], [a,b,2,true], [a,b,2,false], ... ] + public static Object[][] combine(Object[][]... providers) { + int numberOfProviders = providers.length; // (ex: 3) + + // ex: 2 * 2 * 2 combinations + int numberOfCombinations = 1; + for (Object[][] provider : providers) { + numberOfCombinations *= provider.length; + } + + Object[][] result = new Object[numberOfCombinations][]; + // The current index in each provider (ex: [1,0,1] => [c,d,1,false]) + int[] indices = new int[numberOfProviders]; + + for (int c = 0; c < numberOfCombinations; c++) { + int combinationLength = 0; + for (int p = 0; p < numberOfProviders; p++) { + combinationLength += providers[p][indices[p]].length; + } + Object[] combination = new Object[combinationLength]; + int destPos = 0; + for (int p = 0; p < numberOfProviders; p++) { + Object[] src = providers[p][indices[p]]; + System.arraycopy(src, 0, combination, destPos, src.length); + destPos += src.length; + } + result[c] = combination; + + // Update indices: try to increment from the right, if it overflows reset and move left + for (int p = providers.length - 1; p >= 0; p--) { + if (indices[p] < providers[p].length - 1) { + // ex: [0,0,0], p = 2 => [0,0,1] + indices[p] += 1; + break; + } else { + // ex: [0,0,1], p = 2 => [0,0,0], loop to increment to [0,1,0] + indices[p] = 0; + } + } + } + return result; + } + + @DataProvider + public static Object[][] booleans() { + return fromList(true, false); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/api/core/CqlIdentifierTest.java b/core/src/test/java/com/datastax/oss/driver/api/core/CqlIdentifierTest.java new file mode 100644 index 00000000000..db440007e92 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/api/core/CqlIdentifierTest.java @@ -0,0 +1,69 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public class CqlIdentifierTest { + @Test + public void should_build_from_internal() { + assertThat(CqlIdentifier.fromInternal("foo").asInternal()).isEqualTo("foo"); + assertThat(CqlIdentifier.fromInternal("Foo").asInternal()).isEqualTo("Foo"); + assertThat(CqlIdentifier.fromInternal("foo bar").asInternal()).isEqualTo("foo bar"); + assertThat(CqlIdentifier.fromInternal("foo\"bar").asInternal()).isEqualTo("foo\"bar"); + assertThat(CqlIdentifier.fromInternal("create").asInternal()).isEqualTo("create"); + } + + @Test + public void should_build_from_valid_cql() { + assertThat(CqlIdentifier.fromCql("foo").asInternal()).isEqualTo("foo"); + assertThat(CqlIdentifier.fromCql("Foo").asInternal()).isEqualTo("foo"); + assertThat(CqlIdentifier.fromCql("\"Foo\"").asInternal()).isEqualTo("Foo"); + assertThat(CqlIdentifier.fromCql("\"foo bar\"").asInternal()).isEqualTo("foo bar"); + assertThat(CqlIdentifier.fromCql("\"foo\"\"bar\"").asInternal()).isEqualTo("foo\"bar"); + assertThat(CqlIdentifier.fromCql("\"create\"").asInternal()).isEqualTo("create"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_build_from_valid_cql_if_special_characters() { + CqlIdentifier.fromCql("foo bar"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_build_from_valid_cql_if_reserved_keyword() { + CqlIdentifier.fromCql("Create"); + } + + @Test + public void should_format_as_cql() { + assertThat(CqlIdentifier.fromInternal("foo").asCql(false)).isEqualTo("\"foo\""); + assertThat(CqlIdentifier.fromInternal("Foo").asCql(false)).isEqualTo("\"Foo\""); + assertThat(CqlIdentifier.fromInternal("foo bar").asCql(false)).isEqualTo("\"foo bar\""); + assertThat(CqlIdentifier.fromInternal("foo\"bar").asCql(false)).isEqualTo("\"foo\"\"bar\""); + assertThat(CqlIdentifier.fromInternal("create").asCql(false)).isEqualTo("\"create\""); + } + + @Test + public void should_format_as_pretty_cql() { + assertThat(CqlIdentifier.fromInternal("foo").asCql(true)).isEqualTo("foo"); + assertThat(CqlIdentifier.fromInternal("Foo").asCql(true)).isEqualTo("\"Foo\""); + assertThat(CqlIdentifier.fromInternal("foo bar").asCql(true)).isEqualTo("\"foo bar\""); + assertThat(CqlIdentifier.fromInternal("foo\"bar").asCql(true)).isEqualTo("\"foo\"\"bar\""); + assertThat(CqlIdentifier.fromInternal("create").asCql(true)).isEqualTo("\"create\""); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/api/core/VersionAssert.java b/core/src/test/java/com/datastax/oss/driver/api/core/VersionAssert.java new file mode 100644 index 00000000000..1525d95b1da --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/api/core/VersionAssert.java @@ -0,0 +1,65 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import org.assertj.core.api.AbstractComparableAssert; + +public class VersionAssert extends AbstractComparableAssert { + + public VersionAssert(Version actual) { + super(actual, VersionAssert.class); + } + + public VersionAssert hasMajorMinorPatch(int major, int minor, int patch) { + assertThat(actual.getMajor()).isEqualTo(major); + assertThat(actual.getMinor()).isEqualTo(minor); + assertThat(actual.getPatch()).isEqualTo(patch); + return this; + } + + public VersionAssert hasDsePatch(int dsePatch) { + assertThat(actual.getDSEPatch()).isEqualTo(dsePatch); + return this; + } + + public VersionAssert hasPreReleaseLabels(String... labels) { + assertThat(actual.getPreReleaseLabels()).containsExactly(labels); + return this; + } + + public VersionAssert hasNoPreReleaseLabels() { + assertThat(actual.getPreReleaseLabels()).isNull(); + return this; + } + + public VersionAssert hasBuildLabel(String label) { + assertThat(actual.getBuildLabel()).isEqualTo(label); + return this; + } + + public VersionAssert hasNextStable(String version) { + assertThat(actual.nextStable()).isEqualTo(Version.parse(version)); + return this; + } + + @Override + public VersionAssert hasToString(String string) { + assertThat(actual.toString()).isEqualTo(string); + return this; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/api/core/VersionTest.java b/core/src/test/java/com/datastax/oss/driver/api/core/VersionTest.java new file mode 100644 index 00000000000..4a61e246827 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/api/core/VersionTest.java @@ -0,0 +1,103 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import org.junit.Test; + +public class VersionTest { + + @Test + public void should_parse_release_version() { + assertThat(Version.parse("1.2.19")) + .hasMajorMinorPatch(1, 2, 19) + .hasDsePatch(-1) + .hasNoPreReleaseLabels() + .hasBuildLabel(null) + .hasNextStable("1.2.19") + .hasToString("1.2.19"); + } + + @Test + public void should_parse_release_without_patch() { + assertThat(Version.parse("1.2")).hasMajorMinorPatch(1, 2, 0); + } + + @Test + public void should_parse_pre_release_version() { + assertThat(Version.parse("1.2.0-beta1-SNAPSHOT")) + .hasMajorMinorPatch(1, 2, 0) + .hasDsePatch(-1) + .hasPreReleaseLabels("beta1", "SNAPSHOT") + .hasBuildLabel(null) + .hasToString("1.2.0-beta1-SNAPSHOT") + .hasNextStable("1.2.0"); + } + + @Test + public void should_allow_tilde_as_first_pre_release_delimiter() { + assertThat(Version.parse("1.2.0~beta1-SNAPSHOT")) + .hasMajorMinorPatch(1, 2, 0) + .hasDsePatch(-1) + .hasPreReleaseLabels("beta1", "SNAPSHOT") + .hasBuildLabel(null) + .hasToString("1.2.0-beta1-SNAPSHOT") + .hasNextStable("1.2.0"); + } + + @Test + public void should_parse_dse_patch() { + assertThat(Version.parse("1.2.19.2-SNAPSHOT")) + .hasMajorMinorPatch(1, 2, 19) + .hasDsePatch(2) + .hasToString("1.2.19.2-SNAPSHOT") + .hasNextStable("1.2.19.2"); + } + + @Test + public void should_order_versions() { + // by component + assertOrder("1.2.0", "2.0.0", -1); + assertOrder("2.0.0", "2.1.0", -1); + assertOrder("2.0.1", "2.0.2", -1); + assertOrder("2.0.1.1", "2.0.1.2", -1); + + // shortened vs. longer version + assertOrder("2.0", "2.0.0", 0); + assertOrder("2.0", "2.0.1", -1); + + // any DSE version is higher than no DSE version + assertOrder("2.0.0", "2.0.0.0", -1); + assertOrder("2.0.0", "2.0.0.1", -1); + + // pre-release vs. release + assertOrder("2.0.0-beta1", "2.0.0", -1); + assertOrder("2.0.0-SNAPSHOT", "2.0.0", -1); + assertOrder("2.0.0-beta1-SNAPSHOT", "2.0.0", -1); + + // pre-release vs. pre-release + assertOrder("2.0.0-a-b-c", "2.0.0-a-b-d", -1); + assertOrder("2.0.0-a-b-c", "2.0.0-a-b-c-d", -1); + + // build number ignored + assertOrder("2.0.0+build01", "2.0.0+build02", 0); + } + + private void assertOrder(String version1, String version2, int expected) { + assertThat(Version.parse(version1).compareTo(Version.parse(version2))).isEqualTo(expected); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/api/core/data/CqlDurationTest.java b/core/src/test/java/com/datastax/oss/driver/api/core/data/CqlDurationTest.java new file mode 100644 index 00000000000..a880f4a8579 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/api/core/data/CqlDurationTest.java @@ -0,0 +1,166 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.data; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; + +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.UnsupportedTemporalTypeException; +import org.junit.Test; + +public class CqlDurationTest { + + @Test + public void should_parse_from_string_with_standard_pattern() { + assertThat(CqlDuration.from("1y2mo")).isEqualTo(CqlDuration.newInstance(14, 0, 0)); + assertThat(CqlDuration.from("-1y2mo")).isEqualTo(CqlDuration.newInstance(-14, 0, 0)); + assertThat(CqlDuration.from("1Y2MO")).isEqualTo(CqlDuration.newInstance(14, 0, 0)); + assertThat(CqlDuration.from("2w")).isEqualTo(CqlDuration.newInstance(0, 14, 0)); + assertThat(CqlDuration.from("2d10h")) + .isEqualTo(CqlDuration.newInstance(0, 2, 10 * CqlDuration.NANOS_PER_HOUR)); + assertThat(CqlDuration.from("2d")).isEqualTo(CqlDuration.newInstance(0, 2, 0)); + assertThat(CqlDuration.from("30h")) + .isEqualTo(CqlDuration.newInstance(0, 0, 30 * CqlDuration.NANOS_PER_HOUR)); + assertThat(CqlDuration.from("30h20m")) + .isEqualTo( + CqlDuration.newInstance( + 0, 0, 30 * CqlDuration.NANOS_PER_HOUR + 20 * CqlDuration.NANOS_PER_MINUTE)); + assertThat(CqlDuration.from("20m")) + .isEqualTo(CqlDuration.newInstance(0, 0, 20 * CqlDuration.NANOS_PER_MINUTE)); + assertThat(CqlDuration.from("56s")) + .isEqualTo(CqlDuration.newInstance(0, 0, 56 * CqlDuration.NANOS_PER_SECOND)); + assertThat(CqlDuration.from("567ms")) + .isEqualTo(CqlDuration.newInstance(0, 0, 567 * CqlDuration.NANOS_PER_MILLI)); + assertThat(CqlDuration.from("1950us")) + .isEqualTo(CqlDuration.newInstance(0, 0, 1950 * CqlDuration.NANOS_PER_MICRO)); + assertThat(CqlDuration.from("1950µs")) + .isEqualTo(CqlDuration.newInstance(0, 0, 1950 * CqlDuration.NANOS_PER_MICRO)); + assertThat(CqlDuration.from("1950000ns")).isEqualTo(CqlDuration.newInstance(0, 0, 1950000)); + assertThat(CqlDuration.from("1950000NS")).isEqualTo(CqlDuration.newInstance(0, 0, 1950000)); + assertThat(CqlDuration.from("-1950000ns")).isEqualTo(CqlDuration.newInstance(0, 0, -1950000)); + assertThat(CqlDuration.from("1y3mo2h10m")) + .isEqualTo(CqlDuration.newInstance(15, 0, 130 * CqlDuration.NANOS_PER_MINUTE)); + } + + @Test + public void should_parse_from_string_with_iso8601_pattern() { + assertThat(CqlDuration.from("P1Y2D")).isEqualTo(CqlDuration.newInstance(12, 2, 0)); + assertThat(CqlDuration.from("P1Y2M")).isEqualTo(CqlDuration.newInstance(14, 0, 0)); + assertThat(CqlDuration.from("P2W")).isEqualTo(CqlDuration.newInstance(0, 14, 0)); + assertThat(CqlDuration.from("P1YT2H")) + .isEqualTo(CqlDuration.newInstance(12, 0, 2 * CqlDuration.NANOS_PER_HOUR)); + assertThat(CqlDuration.from("-P1Y2M")).isEqualTo(CqlDuration.newInstance(-14, 0, 0)); + assertThat(CqlDuration.from("P2D")).isEqualTo(CqlDuration.newInstance(0, 2, 0)); + assertThat(CqlDuration.from("PT30H")) + .isEqualTo(CqlDuration.newInstance(0, 0, 30 * CqlDuration.NANOS_PER_HOUR)); + assertThat(CqlDuration.from("PT30H20M")) + .isEqualTo( + CqlDuration.newInstance( + 0, 0, 30 * CqlDuration.NANOS_PER_HOUR + 20 * CqlDuration.NANOS_PER_MINUTE)); + assertThat(CqlDuration.from("PT20M")) + .isEqualTo(CqlDuration.newInstance(0, 0, 20 * CqlDuration.NANOS_PER_MINUTE)); + assertThat(CqlDuration.from("PT56S")) + .isEqualTo(CqlDuration.newInstance(0, 0, 56 * CqlDuration.NANOS_PER_SECOND)); + assertThat(CqlDuration.from("P1Y3MT2H10M")) + .isEqualTo(CqlDuration.newInstance(15, 0, 130 * CqlDuration.NANOS_PER_MINUTE)); + } + + @Test + public void should_parse_from_string_with_iso8601_alternative_pattern() { + assertThat(CqlDuration.from("P0001-00-02T00:00:00")) + .isEqualTo(CqlDuration.newInstance(12, 2, 0)); + assertThat(CqlDuration.from("P0001-02-00T00:00:00")) + .isEqualTo(CqlDuration.newInstance(14, 0, 0)); + assertThat(CqlDuration.from("P0001-00-00T02:00:00")) + .isEqualTo(CqlDuration.newInstance(12, 0, 2 * CqlDuration.NANOS_PER_HOUR)); + assertThat(CqlDuration.from("-P0001-02-00T00:00:00")) + .isEqualTo(CqlDuration.newInstance(-14, 0, 0)); + assertThat(CqlDuration.from("P0000-00-02T00:00:00")) + .isEqualTo(CqlDuration.newInstance(0, 2, 0)); + assertThat(CqlDuration.from("P0000-00-00T30:00:00")) + .isEqualTo(CqlDuration.newInstance(0, 0, 30 * CqlDuration.NANOS_PER_HOUR)); + assertThat(CqlDuration.from("P0000-00-00T30:20:00")) + .isEqualTo( + CqlDuration.newInstance( + 0, 0, 30 * CqlDuration.NANOS_PER_HOUR + 20 * CqlDuration.NANOS_PER_MINUTE)); + assertThat(CqlDuration.from("P0000-00-00T00:20:00")) + .isEqualTo(CqlDuration.newInstance(0, 0, 20 * CqlDuration.NANOS_PER_MINUTE)); + assertThat(CqlDuration.from("P0000-00-00T00:00:56")) + .isEqualTo(CqlDuration.newInstance(0, 0, 56 * CqlDuration.NANOS_PER_SECOND)); + assertThat(CqlDuration.from("P0001-03-00T02:10:00")) + .isEqualTo(CqlDuration.newInstance(15, 0, 130 * CqlDuration.NANOS_PER_MINUTE)); + } + + @Test + public void should_fail_to_parse_invalid_durations() { + assertInvalidDuration( + Long.MAX_VALUE + "d", + "Invalid duration. The total number of days must be less or equal to 2147483647"); + assertInvalidDuration("2µ", "Unable to convert '2µ' to a duration"); + assertInvalidDuration("-2µ", "Unable to convert '2µ' to a duration"); + assertInvalidDuration("12.5s", "Unable to convert '12.5s' to a duration"); + assertInvalidDuration("2m12.5s", "Unable to convert '2m12.5s' to a duration"); + assertInvalidDuration("2m-12s", "Unable to convert '2m-12s' to a duration"); + assertInvalidDuration("12s3s", "Invalid duration. The seconds are specified multiple times"); + assertInvalidDuration("12s3m", "Invalid duration. The seconds should be after minutes"); + assertInvalidDuration("1Y3M4D", "Invalid duration. The minutes should be after days"); + assertInvalidDuration("P2Y3W", "Unable to convert 'P2Y3W' to a duration"); + assertInvalidDuration("P0002-00-20", "Unable to convert 'P0002-00-20' to a duration"); + } + + private void assertInvalidDuration(String duration, String expectedErrorMessage) { + try { + CqlDuration.from(duration); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + assertThat(e.getMessage()).isEqualTo(expectedErrorMessage); + } + } + + @Test + public void should_get_by_unit() { + CqlDuration duration = CqlDuration.from("3mo2d15s"); + assertThat(duration.get(ChronoUnit.MONTHS)).isEqualTo(3); + assertThat(duration.get(ChronoUnit.DAYS)).isEqualTo(2); + assertThat(duration.get(ChronoUnit.NANOS)).isEqualTo(15 * CqlDuration.NANOS_PER_SECOND); + assertThatThrownBy(() -> duration.get(ChronoUnit.YEARS)) + .isInstanceOf(UnsupportedTemporalTypeException.class); + } + + @Test + public void should_add_to_temporal() { + ZonedDateTime dateTime = ZonedDateTime.parse("2018-10-04T00:00-07:00[America/Los_Angeles]"); + assertThat(dateTime.plus(CqlDuration.from("1mo"))) + .isEqualTo("2018-11-04T00:00-07:00[America/Los_Angeles]"); + assertThat(dateTime.plus(CqlDuration.from("1mo1h10s"))) + .isEqualTo("2018-11-04T01:00:10-07:00[America/Los_Angeles]"); + // 11-04 2:00 is daylight saving time end + assertThat(dateTime.plus(CqlDuration.from("1mo3h"))) + .isEqualTo("2018-11-04T02:00-08:00[America/Los_Angeles]"); + } + + @Test + public void should_subtract_from_temporal() { + ZonedDateTime dateTime = ZonedDateTime.parse("2018-10-04T00:00-07:00[America/Los_Angeles]"); + assertThat(dateTime.minus(CqlDuration.from("2mo"))) + .isEqualTo("2018-08-04T00:00-07:00[America/Los_Angeles]"); + assertThat(dateTime.minus(CqlDuration.from("1h15s15ns"))) + .isEqualTo("2018-10-03T22:59:44.999999985-07:00[America/Los_Angeles]"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/api/core/retry/DefaultRetryPolicyTest.java b/core/src/test/java/com/datastax/oss/driver/api/core/retry/DefaultRetryPolicyTest.java new file mode 100644 index 00000000000..dac4dcafe20 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/api/core/retry/DefaultRetryPolicyTest.java @@ -0,0 +1,87 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.retry; + +import static com.datastax.oss.driver.api.core.DefaultConsistencyLevel.QUORUM; +import static com.datastax.oss.driver.api.core.retry.RetryDecision.RETHROW; +import static com.datastax.oss.driver.api.core.retry.RetryDecision.RETRY_NEXT; +import static com.datastax.oss.driver.api.core.retry.RetryDecision.RETRY_SAME; +import static com.datastax.oss.driver.api.core.servererrors.DefaultWriteType.BATCH_LOG; +import static com.datastax.oss.driver.api.core.servererrors.DefaultWriteType.SIMPLE; + +import com.datastax.oss.driver.api.core.connection.ClosedConnectionException; +import com.datastax.oss.driver.api.core.connection.HeartbeatException; +import com.datastax.oss.driver.api.core.servererrors.OverloadedException; +import com.datastax.oss.driver.api.core.servererrors.ReadFailureException; +import com.datastax.oss.driver.api.core.servererrors.ServerError; +import com.datastax.oss.driver.api.core.servererrors.TruncateException; +import com.datastax.oss.driver.api.core.servererrors.WriteFailureException; +import com.datastax.oss.driver.internal.core.retry.DefaultRetryPolicy; +import org.junit.Test; + +public class DefaultRetryPolicyTest extends RetryPolicyTestBase { + + public DefaultRetryPolicyTest() { + super(new DefaultRetryPolicy(null, null)); + } + + @Test + public void should_process_read_timeouts() { + assertOnReadTimeout(QUORUM, 2, 2, false, 0).isEqualTo(RETRY_SAME); + assertOnReadTimeout(QUORUM, 2, 2, false, 1).isEqualTo(RETHROW); + assertOnReadTimeout(QUORUM, 2, 2, true, 0).isEqualTo(RETHROW); + assertOnReadTimeout(QUORUM, 2, 1, true, 0).isEqualTo(RETHROW); + assertOnReadTimeout(QUORUM, 2, 1, false, 0).isEqualTo(RETHROW); + } + + @Test + public void should_process_write_timeouts() { + assertOnWriteTimeout(QUORUM, BATCH_LOG, 2, 0, 0).isEqualTo(RETRY_SAME); + assertOnWriteTimeout(QUORUM, BATCH_LOG, 2, 0, 1).isEqualTo(RETHROW); + assertOnWriteTimeout(QUORUM, SIMPLE, 2, 0, 0).isEqualTo(RETHROW); + } + + @Test + public void should_process_unavailable() { + assertOnUnavailable(QUORUM, 2, 1, 0).isEqualTo(RETRY_NEXT); + assertOnUnavailable(QUORUM, 2, 1, 1).isEqualTo(RETHROW); + } + + @Test + public void should_process_aborted_request() { + assertOnRequestAborted(ClosedConnectionException.class, 0).isEqualTo(RETRY_NEXT); + assertOnRequestAborted(ClosedConnectionException.class, 1).isEqualTo(RETRY_NEXT); + assertOnRequestAborted(HeartbeatException.class, 0).isEqualTo(RETRY_NEXT); + assertOnRequestAborted(HeartbeatException.class, 1).isEqualTo(RETRY_NEXT); + assertOnRequestAborted(Throwable.class, 0).isEqualTo(RETHROW); + } + + @Test + public void should_process_error_response() { + assertOnErrorResponse(ReadFailureException.class, 0).isEqualTo(RETHROW); + assertOnErrorResponse(ReadFailureException.class, 1).isEqualTo(RETHROW); + assertOnErrorResponse(WriteFailureException.class, 0).isEqualTo(RETHROW); + assertOnErrorResponse(WriteFailureException.class, 1).isEqualTo(RETHROW); + assertOnErrorResponse(WriteFailureException.class, 1).isEqualTo(RETHROW); + + assertOnErrorResponse(OverloadedException.class, 0).isEqualTo(RETRY_NEXT); + assertOnErrorResponse(OverloadedException.class, 1).isEqualTo(RETRY_NEXT); + assertOnErrorResponse(ServerError.class, 0).isEqualTo(RETRY_NEXT); + assertOnErrorResponse(ServerError.class, 1).isEqualTo(RETRY_NEXT); + assertOnErrorResponse(TruncateException.class, 0).isEqualTo(RETRY_NEXT); + assertOnErrorResponse(TruncateException.class, 1).isEqualTo(RETRY_NEXT); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/api/core/retry/RetryPolicyTestBase.java b/core/src/test/java/com/datastax/oss/driver/api/core/retry/RetryPolicyTestBase.java new file mode 100644 index 00000000000..78c227816e9 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/api/core/retry/RetryPolicyTestBase.java @@ -0,0 +1,66 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.retry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; +import com.datastax.oss.driver.api.core.servererrors.CoordinatorException; +import com.datastax.oss.driver.api.core.servererrors.WriteType; +import com.datastax.oss.driver.api.core.session.Request; +import org.assertj.core.api.Assert; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public abstract class RetryPolicyTestBase { + private final RetryPolicy policy; + + @Mock private Request request; + + protected RetryPolicyTestBase(RetryPolicy policy) { + this.policy = policy; + } + + protected Assert assertOnReadTimeout( + ConsistencyLevel cl, int blockFor, int received, boolean dataPresent, int retryCount) { + return assertThat( + policy.onReadTimeout(request, cl, blockFor, received, dataPresent, retryCount)); + } + + protected Assert assertOnWriteTimeout( + ConsistencyLevel cl, WriteType writeType, int blockFor, int received, int retryCount) { + return assertThat( + policy.onWriteTimeout(request, cl, writeType, blockFor, received, retryCount)); + } + + protected Assert assertOnUnavailable( + ConsistencyLevel cl, int required, int alive, int retryCount) { + return assertThat(policy.onUnavailable(request, cl, required, alive, retryCount)); + } + + protected Assert assertOnRequestAborted( + Class errorClass, int retryCount) { + return assertThat(policy.onRequestAborted(request, mock(errorClass), retryCount)); + } + + protected Assert assertOnErrorResponse( + Class errorClass, int retryCount) { + return assertThat(policy.onErrorResponse(request, mock(errorClass), retryCount)); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/api/core/specex/ConstantSpeculativeExecutionPolicyTest.java b/core/src/test/java/com/datastax/oss/driver/api/core/specex/ConstantSpeculativeExecutionPolicyTest.java new file mode 100644 index 00000000000..7a279dfd99d --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/api/core/specex/ConstantSpeculativeExecutionPolicyTest.java @@ -0,0 +1,79 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.specex; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.internal.core.specex.ConstantSpeculativeExecutionPolicy; +import java.time.Duration; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ConstantSpeculativeExecutionPolicyTest { + @Mock private DriverContext context; + @Mock private DriverConfig config; + @Mock private DriverExecutionProfile defaultProfile; + @Mock private Request request; + + @Before + public void setup() { + when(context.getConfig()).thenReturn(config); + when(config.getProfile(DriverExecutionProfile.DEFAULT_NAME)).thenReturn(defaultProfile); + } + + private void mockOptions(int maxExecutions, long constantDelayMillis) { + when(defaultProfile.getInt(DefaultDriverOption.SPECULATIVE_EXECUTION_MAX)) + .thenReturn(maxExecutions); + when(defaultProfile.getDuration(DefaultDriverOption.SPECULATIVE_EXECUTION_DELAY)) + .thenReturn(Duration.ofMillis(constantDelayMillis)); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_if_delay_negative() { + mockOptions(1, -10); + new ConstantSpeculativeExecutionPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_if_max_less_than_one() { + mockOptions(0, 10); + new ConstantSpeculativeExecutionPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + } + + @Test + public void should_return_delay_until_max() { + mockOptions(3, 10); + SpeculativeExecutionPolicy policy = + new ConstantSpeculativeExecutionPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + + // Initial execution starts, schedule first speculative execution + assertThat(policy.nextExecution(null, null, request, 1)).isEqualTo(10); + // First speculative execution starts, schedule second one + assertThat(policy.nextExecution(null, null, request, 2)).isEqualTo(10); + // Second speculative execution starts, we're at 3 => stop + assertThat(policy.nextExecution(null, null, request, 3)).isNegative(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/api/core/type/UserDefinedTypeTest.java b/core/src/test/java/com/datastax/oss/driver/api/core/type/UserDefinedTypeTest.java new file mode 100644 index 00000000000..05416857057 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/api/core/type/UserDefinedTypeTest.java @@ -0,0 +1,70 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.core.type.UserDefinedTypeBuilder; +import org.junit.Test; + +public class UserDefinedTypeTest { + + private static final UserDefinedType ADDRESS_TYPE = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("test"), CqlIdentifier.fromInternal("address")) + // Not actually used in this test, but UDTs must have fields: + .withField(CqlIdentifier.fromInternal("street"), DataTypes.TEXT) + .frozen() + .build(); + private static final UserDefinedType ACCOUNT_TYPE = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("test"), CqlIdentifier.fromInternal("account")) + .withField(CqlIdentifier.fromInternal("ID"), DataTypes.TEXT) // case-sensitive + .withField(CqlIdentifier.fromInternal("name"), DataTypes.TEXT) + .withField(CqlIdentifier.fromInternal("address"), ADDRESS_TYPE) + .withField( + CqlIdentifier.fromInternal("frozen_list"), DataTypes.frozenListOf(DataTypes.TEXT)) + .withField( + CqlIdentifier.fromInternal("list_of_map"), + DataTypes.listOf(DataTypes.frozenMapOf(DataTypes.TEXT, DataTypes.INT))) + .build(); + + @Test + public void should_describe_as_cql() { + assertThat(ACCOUNT_TYPE.describe(false)) + .isEqualTo( + "CREATE TYPE \"test\".\"account\" ( \"ID\" text, \"name\" text, \"address\" frozen<\"test\".\"address\">, \"frozen_list\" frozen>, \"list_of_map\" list>> );"); + } + + @Test + public void should_describe_as_pretty_cql() { + assertThat(ACCOUNT_TYPE.describe(true)) + .isEqualTo( + "CREATE TYPE test.account (\n" + + " \"ID\" text,\n" + + " name text,\n" + + " address frozen,\n" + + " frozen_list frozen>,\n" + + " list_of_map list>>\n" + + ");"); + } + + @Test + public void should_evaluate_equality() { + assertThat(ACCOUNT_TYPE.newValue()).isEqualTo(ACCOUNT_TYPE.newValue()); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/api/core/type/reflect/GenericTypeTest.java b/core/src/test/java/com/datastax/oss/driver/api/core/type/reflect/GenericTypeTest.java new file mode 100644 index 00000000000..ac66cfca01a --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/api/core/type/reflect/GenericTypeTest.java @@ -0,0 +1,135 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.type.reflect; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.shaded.guava.common.reflect.TypeToken; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.Test; + +public class GenericTypeTest { + + @Test + public void should_wrap_class() { + GenericType stringType = GenericType.of(String.class); + assertThat(stringType.__getToken()).isEqualTo(TypeToken.of(String.class)); + } + + @Test + public void should_capture_generic_type() { + GenericType> stringListType = new GenericType>() {}; + TypeToken> stringListToken = new TypeToken>() {}; + assertThat(stringListType.__getToken()).isEqualTo(stringListToken); + } + + @Test + public void should_wrap_classes_in_collection() { + GenericType> mapType = GenericType.mapOf(String.class, Integer.class); + assertThat(mapType.__getToken()).isEqualTo(new TypeToken>() {}); + } + + @Test + public void should_wrap_types_in_collection() { + GenericType>> mapType = + GenericType.mapOf(GenericType.of(String.class), GenericType.listOf(Integer.class)); + assertThat(mapType.__getToken()).isEqualTo(new TypeToken>>() {}); + } + + @Test + public void should_substitute_type_parameters() { + assertThat(optionalOf(GenericType.listOf(String.class)).__getToken()) + .isEqualTo(new TypeToken>>() {}); + assertThat(mapOf(String.class, Integer.class).__getToken()) + .isEqualTo(new TypeToken>() {}); + } + + @Test + public void should_report_supertype() { + assertThat(GenericType.of(Number.class).isSupertypeOf(GenericType.of(Integer.class))).isTrue(); + assertThat(GenericType.of(Integer.class).isSupertypeOf(GenericType.of(Number.class))).isFalse(); + } + + @Test + public void should_report_subtype() { + assertThat(GenericType.of(Number.class).isSubtypeOf(GenericType.of(Integer.class))).isFalse(); + assertThat(GenericType.of(Integer.class).isSubtypeOf(GenericType.of(Number.class))).isTrue(); + } + + @Test + public void should_wrap_primitive_type() { + assertThat(GenericType.of(Integer.TYPE).wrap()).isEqualTo(GenericType.of(Integer.class)); + GenericType stringType = GenericType.of(String.class); + assertThat(stringType.wrap()).isSameAs(stringType); + } + + @Test + public void should_unwrap_wrapper_type() { + assertThat(GenericType.of(Integer.class).unwrap()).isEqualTo(GenericType.of(Integer.TYPE)); + GenericType stringType = GenericType.of(String.class); + assertThat(stringType.unwrap()).isSameAs(stringType); + } + + @Test + public void should_return_raw_type() { + assertThat(GenericType.INTEGER.getRawType()).isEqualTo(Integer.class); + assertThat(GenericType.listOf(Integer.class).getRawType()).isEqualTo(List.class); + } + + @Test + public void should_return_super_type() { + GenericType> expectedType = iterableOf(GenericType.INTEGER); + assertThat(GenericType.listOf(Integer.class).getSupertype(Iterable.class)) + .isEqualTo(expectedType); + } + + @Test + public void should_return_sub_type() { + GenericType> superType = iterableOf(GenericType.INTEGER); + assertThat(superType.getSubtype(List.class)).isEqualTo(GenericType.listOf(GenericType.INTEGER)); + } + + @Test + public void should_return_type() { + assertThat(GenericType.INTEGER.getType()).isEqualTo(Integer.class); + } + + @Test + public void should_return_component_type() { + assertThat(GenericType.of(Integer[].class).getComponentType()).isEqualTo(GenericType.INTEGER); + } + + @Test + public void should_report_is_array() { + assertThat(GenericType.INTEGER.isArray()).isFalse(); + assertThat(GenericType.of(Integer[].class).isArray()).isTrue(); + } + + private GenericType> optionalOf(GenericType elementType) { + return new GenericType>() {}.where(new GenericTypeParameter() {}, elementType); + } + + private GenericType> iterableOf(GenericType elementType) { + return new GenericType>() {}.where(new GenericTypeParameter() {}, elementType); + } + + private GenericType> mapOf(Class keyClass, Class valueClass) { + return new GenericType>() {}.where(new GenericTypeParameter() {}, keyClass) + .where(new GenericTypeParameter() {}, valueClass); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/api/core/uuid/UuidsTest.java b/core/src/test/java/com/datastax/oss/driver/api/core/uuid/UuidsTest.java new file mode 100644 index 00000000000..da51e00f366 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/api/core/uuid/UuidsTest.java @@ -0,0 +1,173 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.core.uuid; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentSkipListSet; +import org.junit.Test; + +public class UuidsTest { + + @Test + public void should_generate_timestamp_within_10_ms() { + + // The Uuids class does some computation at class initialization, which may screw up our + // assumption below that Uuids.timeBased() takes less than 10ms, so force class loading now. + Uuids.random(); + + long start = System.currentTimeMillis(); + UUID uuid = Uuids.timeBased(); + + assertThat(uuid.version()).isEqualTo(1); + assertThat(uuid.variant()).isEqualTo(2); + + long timestamp = Uuids.unixTimestamp(uuid); + + assertThat(timestamp) + .as("Generated timestamp should be within 10 ms") + .isBetween(start, start + 10); + } + + @Test + public void should_generate_unique_time_based_uuids() { + int count = 1_000_000; + Set generated = new HashSet<>(count); + + for (int i = 0; i < count; ++i) { + generated.add(Uuids.timeBased()); + } + + assertThat(generated).hasSize(count); + } + + @Test + public void should_generate_unique_time_based_uuids_across_threads() throws Exception { + int threadCount = 10; + int uuidsPerThread = 10_000; + Set generated = new ConcurrentSkipListSet<>(); + + UUIDGenerator[] generators = new UUIDGenerator[threadCount]; + for (int i = 0; i < threadCount; i++) { + generators[i] = new UUIDGenerator(uuidsPerThread, generated); + } + for (int i = 0; i < threadCount; i++) { + generators[i].start(); + } + for (int i = 0; i < threadCount; i++) { + generators[i].join(); + } + + assertThat(generated).hasSize(threadCount * uuidsPerThread); + } + + @Test + public void should_generate_ever_increasing_timestamps() { + int count = 1_000_000; + long previous = 0; + for (int i = 0; i < count; i++) { + long current = Uuids.timeBased().timestamp(); + assertThat(current).isGreaterThan(previous); + previous = current; + } + } + + @Test + public void should_generate_within_bounds_for_given_timestamp() { + + Random random = new Random(System.currentTimeMillis()); + + int timestampsCount = 10; + int uuidsPerTimestamp = 10; + + for (int i = 0; i < timestampsCount; i++) { + long timestamp = (long) random.nextInt(); + for (int j = 0; j < uuidsPerTimestamp; j++) { + UUID uuid = new UUID(Uuids.makeMsb(Uuids.fromUnixTimestamp(timestamp)), random.nextLong()); + assertBetween(uuid, Uuids.startOf(timestamp), Uuids.endOf(timestamp)); + } + } + } + + // Compares using Cassandra's sorting algorithm (not the same as compareTo). + private static void assertBetween(UUID uuid, UUID lowerBound, UUID upperBound) { + ByteBuffer uuidBytes = TypeCodecs.UUID.encode(uuid, DefaultProtocolVersion.V3); + ByteBuffer lb = TypeCodecs.UUID.encode(lowerBound, DefaultProtocolVersion.V3); + ByteBuffer ub = TypeCodecs.UUID.encode(upperBound, DefaultProtocolVersion.V3); + assertThat(compareTimestampBytes(lb, uuidBytes)).isLessThanOrEqualTo(0); + assertThat(compareTimestampBytes(ub, uuidBytes)).isGreaterThanOrEqualTo(0); + } + + private static int compareTimestampBytes(ByteBuffer o1, ByteBuffer o2) { + int o1Pos = o1.position(); + int o2Pos = o2.position(); + + int d = (o1.get(o1Pos + 6) & 0xF) - (o2.get(o2Pos + 6) & 0xF); + if (d != 0) { + return d; + } + d = (o1.get(o1Pos + 7) & 0xFF) - (o2.get(o2Pos + 7) & 0xFF); + if (d != 0) { + return d; + } + d = (o1.get(o1Pos + 4) & 0xFF) - (o2.get(o2Pos + 4) & 0xFF); + if (d != 0) { + return d; + } + d = (o1.get(o1Pos + 5) & 0xFF) - (o2.get(o2Pos + 5) & 0xFF); + if (d != 0) { + return d; + } + d = (o1.get(o1Pos) & 0xFF) - (o2.get(o2Pos) & 0xFF); + if (d != 0) { + return d; + } + d = (o1.get(o1Pos + 1) & 0xFF) - (o2.get(o2Pos + 1) & 0xFF); + if (d != 0) { + return d; + } + d = (o1.get(o1Pos + 2) & 0xFF) - (o2.get(o2Pos + 2) & 0xFF); + if (d != 0) { + return d; + } + return (o1.get(o1Pos + 3) & 0xFF) - (o2.get(o2Pos + 3) & 0xFF); + } + + private static class UUIDGenerator extends Thread { + + private final int toGenerate; + private final Set generated; + + UUIDGenerator(int toGenerate, Set generated) { + this.toGenerate = toGenerate; + this.generated = generated; + } + + @Override + public void run() { + for (int i = 0; i < toGenerate; ++i) { + generated.add(Uuids.timeBased()); + } + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/SerializationHelper.java b/core/src/test/java/com/datastax/oss/driver/internal/SerializationHelper.java new file mode 100644 index 00000000000..0494aeaf539 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/SerializationHelper.java @@ -0,0 +1,56 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal; + +import static org.assertj.core.api.Assertions.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +public abstract class SerializationHelper { + + public static byte[] serialize(T t) { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(bytes); + out.writeObject(t); + return bytes.toByteArray(); + } catch (Exception e) { + fail("Unexpected error", e); + throw new AssertionError(); // never reached + } + } + + // the calling code performs validations on the result, so this doesn't matter + @SuppressWarnings("TypeParameterUnusedInFormals") + public static T deserialize(byte[] bytes) { + try { + ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes)); + @SuppressWarnings("unchecked") + T t = (T) in.readObject(); + return t; + } catch (Exception e) { + fail("Unexpected error", e); + throw new AssertionError(); // never reached + } + } + + public static T serializeAndDeserialize(T t) { + return deserialize(serialize(t)); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/AsyncPagingIterableWrapperTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/AsyncPagingIterableWrapperTest.java new file mode 100644 index 00000000000..554e23c7720 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/AsyncPagingIterableWrapperTest.java @@ -0,0 +1,141 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.MappedAsyncPagingIterable; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.cql.DefaultAsyncResultSet; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class AsyncPagingIterableWrapperTest { + + @Mock private ColumnDefinitions columnDefinitions; + @Mock private Statement statement; + @Mock private CqlSession session; + @Mock private InternalDriverContext context; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + // One single column "i" of type int: + when(columnDefinitions.contains("i")).thenReturn(true); + ColumnDefinition iDefinition = mock(ColumnDefinition.class); + when(iDefinition.getType()).thenReturn(DataTypes.INT); + when(columnDefinitions.get("i")).thenReturn(iDefinition); + when(columnDefinitions.firstIndexOf("i")).thenReturn(0); + when(columnDefinitions.get(0)).thenReturn(iDefinition); + + when(context.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT); + when(context.getProtocolVersion()).thenReturn(DefaultProtocolVersion.DEFAULT); + } + + @Test + public void should_wrap_result_set() throws Exception { + // Given + // two pages of data: + ExecutionInfo executionInfo1 = mockExecutionInfo(); + DefaultAsyncResultSet resultSet1 = + new DefaultAsyncResultSet( + columnDefinitions, executionInfo1, mockData(0, 5), session, context); + DefaultAsyncResultSet resultSet2 = + new DefaultAsyncResultSet( + columnDefinitions, mockExecutionInfo(), mockData(5, 10), session, context); + // chain them together: + ByteBuffer mockPagingState = ByteBuffer.allocate(0); + when(executionInfo1.getPagingState()).thenReturn(mockPagingState); + Statement mockNextStatement = mock(Statement.class); + when(((Statement) statement).copy(mockPagingState)).thenReturn(mockNextStatement); + when(session.executeAsync(mockNextStatement)) + .thenAnswer(invocation -> CompletableFuture.completedFuture(resultSet2)); + + // When + MappedAsyncPagingIterable iterable1 = resultSet1.map(row -> row.getInt("i")); + + // Then + for (int i = 0; i < 5; i++) { + assertThat(iterable1.one()).isEqualTo(i); + assertThat(iterable1.remaining()).isEqualTo(resultSet1.remaining()).isEqualTo(4 - i); + } + assertThat(iterable1.hasMorePages()).isTrue(); + + MappedAsyncPagingIterable iterable2 = + iterable1.fetchNextPage().toCompletableFuture().get(); + for (int i = 5; i < 10; i++) { + assertThat(iterable2.one()).isEqualTo(i); + assertThat(iterable2.remaining()).isEqualTo(resultSet2.remaining()).isEqualTo(9 - i); + } + assertThat(iterable2.hasMorePages()).isFalse(); + } + + /** Checks that consuming from the wrapper consumes from the source, and vice-versa. */ + @Test + public void should_share_iteration_progress_with_wrapped_result_set() { + // Given + DefaultAsyncResultSet resultSet = + new DefaultAsyncResultSet( + columnDefinitions, mockExecutionInfo(), mockData(0, 10), session, context); + + // When + MappedAsyncPagingIterable iterable = resultSet.map(row -> row.getInt("i")); + + // Then + // Consume alternatively from the source and mapped iterable, and check that they stay in sync + for (int i = 0; i < 10; i++) { + Object element = (i % 2 == 0 ? resultSet : iterable).one(); + assertThat(element).isNotNull(); + assertThat(iterable.remaining()).isEqualTo(resultSet.remaining()).isEqualTo(9 - i); + } + assertThat(resultSet.hasMorePages()).isFalse(); + assertThat(iterable.hasMorePages()).isFalse(); + } + + private ExecutionInfo mockExecutionInfo() { + ExecutionInfo executionInfo = mock(ExecutionInfo.class); + when(executionInfo.getStatement()).thenAnswer(invocation -> statement); + return executionInfo; + } + + private Queue> mockData(int start, int end) { + Queue> data = new ArrayDeque<>(); + for (int i = start; i < end; i++) { + data.add(Lists.newArrayList(TypeCodecs.INT.encode(i, DefaultProtocolVersion.DEFAULT))); + } + return data; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/CassandraProtocolVersionRegistryHighestCommonTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/CassandraProtocolVersionRegistryHighestCommonTest.java new file mode 100644 index 00000000000..19146e6c286 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/CassandraProtocolVersionRegistryHighestCommonTest.java @@ -0,0 +1,107 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException; +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.Collection; +import java.util.Collections; +import org.junit.Test; + +/** + * Covers {@link CassandraProtocolVersionRegistry#highestCommon(Collection)} separately, because it + * relies explicitly on {@link DefaultProtocolVersion} as the version implementation. + */ +public class CassandraProtocolVersionRegistryHighestCommonTest { + + private CassandraProtocolVersionRegistry registry = new CassandraProtocolVersionRegistry("test"); + + @Test + public void should_pick_v3_when_at_least_one_node_is_2_1() { + assertThat( + registry.highestCommon( + ImmutableList.of(mockNode("2.2.1"), mockNode("2.1.0"), mockNode("3.1.9")))) + .isEqualTo(DefaultProtocolVersion.V3); + } + + @Test + public void should_pick_v4_when_all_nodes_are_2_2_or_more() { + assertThat( + registry.highestCommon( + ImmutableList.of(mockNode("2.2.0"), mockNode("2.2.1"), mockNode("3.1.9")))) + .isEqualTo(DefaultProtocolVersion.V4); + } + + @Test + public void should_treat_rcs_as_next_stable_versions() { + assertThat( + registry.highestCommon( + ImmutableList.of(mockNode("2.2.1"), mockNode("2.1.0-rc1"), mockNode("3.1.9")))) + .isEqualTo(DefaultProtocolVersion.V3); + assertThat( + registry.highestCommon( + ImmutableList.of(mockNode("2.2.0-rc2"), mockNode("2.2.1"), mockNode("3.1.9")))) + .isEqualTo(DefaultProtocolVersion.V4); + } + + @Test + public void should_skip_nodes_that_report_null_version() { + assertThat( + registry.highestCommon( + ImmutableList.of(mockNode(null), mockNode("2.1.0"), mockNode("3.1.9")))) + .isEqualTo(DefaultProtocolVersion.V3); + + // Edge case: if all do, go with the latest version + assertThat( + registry.highestCommon( + ImmutableList.of(mockNode(null), mockNode(null), mockNode(null)))) + .isEqualTo(DefaultProtocolVersion.V4); + } + + @Test + public void should_use_v4_for_future_cassandra_versions() { + // That might change in the future when some C* versions drop v4 support + assertThat( + registry.highestCommon( + ImmutableList.of(mockNode("3.0.0"), mockNode("12.1.5"), mockNode("98.7.22")))) + .isEqualTo(DefaultProtocolVersion.V4); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_if_no_nodes() { + registry.highestCommon(Collections.emptyList()); + } + + private Node mockNode(String cassandraVersion) { + Node node = mock(Node.class); + if (cassandraVersion != null) { + when(node.getCassandraVersion()).thenReturn(Version.parse(cassandraVersion)); + } + return node; + } + + @Test(expected = UnsupportedProtocolVersionException.class) + public void should_fail_if_pre_2_1_node() { + registry.highestCommon(ImmutableList.of(mockNode("3.0.0"), mockNode("2.0.9"))); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/CassandraProtocolVersionRegistryTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/CassandraProtocolVersionRegistryTest.java new file mode 100644 index 00000000000..0835e1c83ab --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/CassandraProtocolVersionRegistryTest.java @@ -0,0 +1,121 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Optional; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * Covers the method that are agnostic to the actual {@link ProtocolVersion} implementation (using a + * mock implementation). + */ +public class CassandraProtocolVersionRegistryTest { + + private static ProtocolVersion V3 = new MockProtocolVersion(3, false); + private static ProtocolVersion V4 = new MockProtocolVersion(4, false); + private static ProtocolVersion V5 = new MockProtocolVersion(5, false); + private static ProtocolVersion V5_BETA = new MockProtocolVersion(5, true); + private static ProtocolVersion V10 = new MockProtocolVersion(10, false); + private static ProtocolVersion V11 = new MockProtocolVersion(11, false); + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void should_fail_if_duplicate_version_code() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Duplicate version code: 5 in V5 and V5_BETA"); + new CassandraProtocolVersionRegistry("test", new ProtocolVersion[] {V5, V5_BETA}); + } + + @Test + public void should_find_version_by_name() { + ProtocolVersionRegistry versions = + new CassandraProtocolVersionRegistry("test", new ProtocolVersion[] {V3, V4}); + assertThat(versions.fromName("V3")).isEqualTo(V3); + assertThat(versions.fromName("V4")).isEqualTo(V4); + } + + @Test + public void should_downgrade_if_lower_version_available() { + ProtocolVersionRegistry versions = + new CassandraProtocolVersionRegistry("test", new ProtocolVersion[] {V3, V4}); + Optional downgraded = versions.downgrade(V4); + downgraded.map(version -> assertThat(version).isEqualTo(V3)).orElseThrow(AssertionError::new); + } + + @Test + public void should_not_downgrade_if_no_lower_version() { + ProtocolVersionRegistry versions = + new CassandraProtocolVersionRegistry("test", new ProtocolVersion[] {V3, V4}); + Optional downgraded = versions.downgrade(V3); + assertThat(downgraded.isPresent()).isFalse(); + } + + @Test + public void should_downgrade_across_version_range() { + ProtocolVersionRegistry versions = + new CassandraProtocolVersionRegistry( + "test", new ProtocolVersion[] {V3, V4}, new ProtocolVersion[] {V10, V11}); + Optional downgraded = versions.downgrade(V10); + downgraded.map(version -> assertThat(version).isEqualTo(V4)).orElseThrow(AssertionError::new); + } + + @Test + public void should_downgrade_skipping_beta_version() { + ProtocolVersionRegistry versions = + new CassandraProtocolVersionRegistry( + "test", new ProtocolVersion[] {V4, V5_BETA}, new ProtocolVersion[] {V10, V11}); + Optional downgraded = versions.downgrade(V10); + downgraded.map(version -> assertThat(version).isEqualTo(V4)).orElseThrow(AssertionError::new); + } + + private static class MockProtocolVersion implements ProtocolVersion { + private final int code; + private final boolean beta; + + MockProtocolVersion(int code, boolean beta) { + this.code = code; + this.beta = beta; + } + + @Override + public int getCode() { + return code; + } + + @NonNull + @Override + public String name() { + return "V" + code; + } + + @Override + public boolean isBeta() { + return beta; + } + + @Override + public String toString() { + return name() + (beta ? "_BETA" : ""); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/CompletionStageAssert.java b/core/src/test/java/com/datastax/oss/driver/internal/core/CompletionStageAssert.java new file mode 100644 index 00000000000..0484386cd8f --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/CompletionStageAssert.java @@ -0,0 +1,111 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import org.assertj.core.api.AbstractAssert; + +public class CompletionStageAssert + extends AbstractAssert, CompletionStage> { + + public CompletionStageAssert(CompletionStage actual) { + super(actual, CompletionStageAssert.class); + } + + public CompletionStageAssert isSuccess(Consumer valueAssertions) { + try { + V value = actual.toCompletableFuture().get(2, TimeUnit.SECONDS); + valueAssertions.accept(value); + } catch (TimeoutException e) { + fail("Future did not complete within the timeout"); + } catch (Throwable t) { + fail("Unexpected error while waiting on the future", t); + } + return this; + } + + public CompletionStageAssert isSuccess() { + return isSuccess(v -> {}); + } + + public CompletionStageAssert isFailed(Consumer failureAssertions) { + try { + actual.toCompletableFuture().get(2, TimeUnit.SECONDS); + fail("Expected completion stage to fail"); + } catch (TimeoutException e) { + fail("Future did not complete within the timeout"); + } catch (InterruptedException e) { + fail("Interrupted while waiting for future to fail"); + } catch (ExecutionException e) { + failureAssertions.accept(e.getCause()); + } + return this; + } + + public CompletionStageAssert isFailed() { + return isFailed(f -> {}); + } + + public CompletionStageAssert isCancelled() { + boolean cancelled = false; + try { + actual.toCompletableFuture().get(2, TimeUnit.SECONDS); + } catch (CancellationException e) { + cancelled = true; + } catch (Exception ignored) { + } + if (!cancelled) { + fail("Expected completion stage to be cancelled"); + } + return this; + } + + public CompletionStageAssert isNotCancelled() { + boolean cancelled = false; + try { + actual.toCompletableFuture().get(2, TimeUnit.SECONDS); + } catch (CancellationException e) { + cancelled = true; + } catch (Exception ignored) { + } + if (cancelled) { + fail("Expected completion stage not to be cancelled"); + } + return this; + } + + public CompletionStageAssert isDone() { + assertThat(actual.toCompletableFuture().isDone()) + .overridingErrorMessage("Expected completion stage to be done") + .isTrue(); + return this; + } + + public CompletionStageAssert isNotDone() { + assertThat(actual.toCompletableFuture().isDone()) + .overridingErrorMessage("Expected completion stage not to be done") + .isFalse(); + return this; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/DriverConfigAssert.java b/core/src/test/java/com/datastax/oss/driver/internal/core/DriverConfigAssert.java new file mode 100644 index 00000000000..8da0b24ecb2 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/DriverConfigAssert.java @@ -0,0 +1,38 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverOption; +import org.assertj.core.api.AbstractAssert; + +public class DriverConfigAssert extends AbstractAssert { + public DriverConfigAssert(DriverConfig actual) { + super(actual, DriverConfigAssert.class); + } + + public DriverConfigAssert hasIntOption(DriverOption option, int expected) { + assertThat(actual.getDefaultProfile().getInt(option)).isEqualTo(expected); + return this; + } + + public DriverConfigAssert hasIntOption(String profileName, DriverOption option, int expected) { + assertThat(actual.getProfile(profileName).getInt(option)).isEqualTo(expected); + return this; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/NettyFutureAssert.java b/core/src/test/java/com/datastax/oss/driver/internal/core/NettyFutureAssert.java new file mode 100644 index 00000000000..0e572b89f93 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/NettyFutureAssert.java @@ -0,0 +1,72 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import io.netty.util.concurrent.Future; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import org.assertj.core.api.AbstractAssert; + +public class NettyFutureAssert extends AbstractAssert, Future> { + + public NettyFutureAssert(Future actual) { + super(actual, NettyFutureAssert.class); + } + + public NettyFutureAssert isNotDone() { + assertThat(actual.isDone()).isFalse(); + return this; + } + + public NettyFutureAssert isSuccess(Consumer valueAssertions) { + try { + V value = actual.get(100, TimeUnit.MILLISECONDS); + valueAssertions.accept(value); + } catch (TimeoutException e) { + fail("Future did not complete within the timeout"); + } catch (Throwable t) { + fail("Unexpected error while waiting on the future", t); + } + return this; + } + + public NettyFutureAssert isSuccess() { + return isSuccess(v -> {}); + } + + public NettyFutureAssert isFailed(Consumer failureAssertions) { + try { + actual.get(100, TimeUnit.MILLISECONDS); + fail("Expected future to fail"); + } catch (TimeoutException e) { + fail("Future did not fail within the timeout"); + } catch (InterruptedException e) { + fail("Interrupted while waiting for future to fail"); + } catch (ExecutionException e) { + failureAssertions.accept(e.getCause()); + } + return this; + } + + public NettyFutureAssert isFailed() { + return isFailed(f -> {}); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/PagingIterableWrapperTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/PagingIterableWrapperTest.java new file mode 100644 index 00000000000..85718052928 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/PagingIterableWrapperTest.java @@ -0,0 +1,105 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.PagingIterable; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.internal.core.cql.ResultSetTestBase; +import com.datastax.oss.driver.internal.core.cql.ResultSets; +import java.util.Iterator; +import org.junit.Test; + +public class PagingIterableWrapperTest extends ResultSetTestBase { + + @Test + public void should_wrap_result_set() { + // Given + AsyncResultSet page1 = mockPage(true, 0, 1, 2); + AsyncResultSet page2 = mockPage(true, 3, 4, 5); + AsyncResultSet page3 = mockPage(false, 6, 7, 8); + + complete(page1.fetchNextPage(), page2); + complete(page2.fetchNextPage(), page3); + + // When + PagingIterable iterable = ResultSets.newInstance(page1).map(row -> row.getInt(0)); + + // Then + assertThat(iterable.getExecutionInfo()).isSameAs(page1.getExecutionInfo()); + assertThat(iterable.getExecutionInfos()).containsExactly(page1.getExecutionInfo()); + + Iterator iterator = iterable.iterator(); + + assertThat(iterator.next()).isEqualTo(0); + assertThat(iterator.next()).isEqualTo(1); + assertThat(iterator.next()).isEqualTo(2); + + assertThat(iterator.hasNext()).isTrue(); + // This should have triggered the fetch of page2 + assertThat(iterable.getExecutionInfo()).isEqualTo(page2.getExecutionInfo()); + assertThat(iterable.getExecutionInfos()) + .containsExactly(page1.getExecutionInfo(), page2.getExecutionInfo()); + + assertThat(iterator.next()).isEqualTo(3); + assertThat(iterator.next()).isEqualTo(4); + assertThat(iterator.next()).isEqualTo(5); + + assertThat(iterator.hasNext()).isTrue(); + // This should have triggered the fetch of page3 + assertThat(iterable.getExecutionInfo()).isEqualTo(page3.getExecutionInfo()); + assertThat(iterable.getExecutionInfos()) + .containsExactly( + page1.getExecutionInfo(), page2.getExecutionInfo(), page3.getExecutionInfo()); + + assertThat(iterator.next()).isEqualTo(6); + assertThat(iterator.next()).isEqualTo(7); + assertThat(iterator.next()).isEqualTo(8); + } + + /** Checks that consuming from the wrapper consumes from the source, and vice-versa. */ + @Test + public void should_share_iteration_progress_with_wrapped_result_set() { + // Given + AsyncResultSet page1 = mockPage(true, 0, 1, 2); + AsyncResultSet page2 = mockPage(true, 3, 4, 5); + AsyncResultSet page3 = mockPage(false, 6, 7, 8); + + complete(page1.fetchNextPage(), page2); + complete(page2.fetchNextPage(), page3); + + // When + ResultSet resultSet = ResultSets.newInstance(page1); + PagingIterable iterable = resultSet.map(row -> row.getInt(0)); + + // Then + Iterator sourceIterator = resultSet.iterator(); + Iterator mappedIterator = iterable.iterator(); + + assertThat(mappedIterator.next()).isEqualTo(0); + assertNextRow(sourceIterator, 1); + assertThat(mappedIterator.next()).isEqualTo(2); + assertNextRow(sourceIterator, 3); + assertThat(mappedIterator.next()).isEqualTo(4); + assertNextRow(sourceIterator, 5); + assertThat(mappedIterator.next()).isEqualTo(6); + assertNextRow(sourceIterator, 7); + assertThat(mappedIterator.next()).isEqualTo(8); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/TestResponses.java b/core/src/test/java/com/datastax/oss/driver/internal/core/TestResponses.java new file mode 100644 index 00000000000..ecb84c0aced --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/TestResponses.java @@ -0,0 +1,46 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core; + +import com.datastax.oss.driver.shaded.guava.common.base.Charsets; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.result.ColumnSpec; +import com.datastax.oss.protocol.internal.response.result.DefaultRows; +import com.datastax.oss.protocol.internal.response.result.RawType; +import com.datastax.oss.protocol.internal.response.result.Rows; +import com.datastax.oss.protocol.internal.response.result.RowsMetadata; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Queue; + +public class TestResponses { + /** The response to the query run by each connection to check if the cluster name matches. */ + public static Rows clusterNameResponse(String actualClusterName) { + ColumnSpec colSpec = + new ColumnSpec( + "system", + "local", + "cluster_name", + 0, + RawType.PRIMITIVES.get(ProtocolConstants.DataType.VARCHAR)); + RowsMetadata metadata = new RowsMetadata(ImmutableList.of(colSpec), null, null, null); + Queue> data = Lists.newLinkedList(); + data.add(Lists.newArrayList(ByteBuffer.wrap(actualClusterName.getBytes(Charsets.UTF_8)))); + return new DefaultRows(metadata, data); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/Ec2MultiRegionAddressTranslatorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/Ec2MultiRegionAddressTranslatorTest.java new file mode 100644 index 00000000000..2b2f7cf5eb8 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/addresstranslation/Ec2MultiRegionAddressTranslatorTest.java @@ -0,0 +1,94 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.addresstranslation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import javax.naming.NamingException; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.InitialDirContext; +import org.junit.Test; + +public class Ec2MultiRegionAddressTranslatorTest { + + @Test + public void should_return_same_address_when_no_entry_found() throws Exception { + InitialDirContext mock = mock(InitialDirContext.class); + when(mock.getAttributes(anyString(), any(String[].class))).thenReturn(new BasicAttributes()); + Ec2MultiRegionAddressTranslator translator = new Ec2MultiRegionAddressTranslator(mock); + + InetSocketAddress address = new InetSocketAddress("192.0.2.5", 9042); + assertThat(translator.translate(address)).isEqualTo(address); + } + + @Test + public void should_return_same_address_when_exception_encountered() throws Exception { + InitialDirContext mock = mock(InitialDirContext.class); + when(mock.getAttributes(anyString(), any(String[].class))) + .thenThrow(new NamingException("Problem resolving address (not really).")); + Ec2MultiRegionAddressTranslator translator = new Ec2MultiRegionAddressTranslator(mock); + + InetSocketAddress address = new InetSocketAddress("192.0.2.5", 9042); + assertThat(translator.translate(address)).isEqualTo(address); + } + + @Test + public void should_return_new_address_when_match_found() throws Exception { + InetSocketAddress expectedAddress = new InetSocketAddress("54.32.55.66", 9042); + + InitialDirContext mock = mock(InitialDirContext.class); + when(mock.getAttributes("5.2.0.192.in-addr.arpa", new String[] {"PTR"})) + .thenReturn(new BasicAttributes("PTR", expectedAddress.getHostName())); + Ec2MultiRegionAddressTranslator translator = new Ec2MultiRegionAddressTranslator(mock); + + InetSocketAddress address = new InetSocketAddress("192.0.2.5", 9042); + assertThat(translator.translate(address)).isEqualTo(expectedAddress); + } + + @Test + public void should_close_context_when_closed() throws Exception { + InitialDirContext mock = mock(InitialDirContext.class); + Ec2MultiRegionAddressTranslator translator = new Ec2MultiRegionAddressTranslator(mock); + + // ensure close has not been called to this point. + verify(mock, times(0)).close(); + translator.close(); + // ensure close is closed. + verify(mock).close(); + } + + @Test + public void should_build_reversed_domain_name_for_ip_v4() throws Exception { + InetAddress address = InetAddress.getByName("192.0.2.5"); + assertThat(Ec2MultiRegionAddressTranslator.reverse(address)) + .isEqualTo("5.2.0.192.in-addr.arpa"); + } + + @Test + public void should_build_reversed_domain_name_for_ip_v6() throws Exception { + InetAddress address = InetAddress.getByName("2001:db8::567:89ab"); + assertThat(Ec2MultiRegionAddressTranslator.reverse(address)) + .isEqualTo("b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryAvailableIdsTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryAvailableIdsTest.java new file mode 100644 index 00000000000..1f9ad10478a --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryAvailableIdsTest.java @@ -0,0 +1,86 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.internal.core.metrics.NoopNodeMetricUpdater; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.request.Query; +import com.datastax.oss.protocol.internal.response.result.Void; +import io.netty.util.concurrent.Future; +import java.util.concurrent.CompletionStage; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class ChannelFactoryAvailableIdsTest extends ChannelFactoryTestBase { + + @Mock private ResponseCallback responseCallback; + + @Before + @Override + public void setup() throws InterruptedException { + super.setup(); + when(defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn(true); + when(defaultProfile.getString(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn("V4"); + when(protocolVersionRegistry.fromName("V4")).thenReturn(DefaultProtocolVersion.V4); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_MAX_REQUESTS)).thenReturn(128); + + when(responseCallback.isLastResponse(any(Frame.class))).thenReturn(true); + } + + @Test + public void should_report_available_ids() { + // Given + ChannelFactory factory = newChannelFactory(); + + // When + CompletionStage channelFuture = + factory.connect( + SERVER_ADDRESS, DriverChannelOptions.builder().build(), NoopNodeMetricUpdater.INSTANCE); + completeSimpleChannelInit(); + + // Then + assertThatStage(channelFuture) + .isSuccess( + channel -> { + assertThat(channel.getAvailableIds()).isEqualTo(128); + + // Write a request, should decrease the count + Future writeFuture = + channel.write(new Query("test"), false, Frame.NO_PAYLOAD, responseCallback); + assertThat(writeFuture) + .isSuccess( + v -> { + assertThat(channel.getAvailableIds()).isEqualTo(127); + + // Complete the request, should increase again + writeInboundFrame(readOutboundFrame(), Void.INSTANCE); + verify(responseCallback, timeout(500)).onResponse(any(Frame.class)); + assertThat(channel.getAvailableIds()).isEqualTo(128); + }); + }); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryClusterNameTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryClusterNameTest.java new file mode 100644 index 00000000000..f61b0501c61 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryClusterNameTest.java @@ -0,0 +1,95 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.internal.core.TestResponses; +import com.datastax.oss.driver.internal.core.metrics.NoopNodeMetricUpdater; +import com.datastax.oss.protocol.internal.response.Ready; +import java.util.concurrent.CompletionStage; +import org.junit.Test; + +public class ChannelFactoryClusterNameTest extends ChannelFactoryTestBase { + + @Test + public void should_set_cluster_name_from_first_connection() { + // Given + when(defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn(false); + when(protocolVersionRegistry.highestNonBeta()).thenReturn(DefaultProtocolVersion.V4); + ChannelFactory factory = newChannelFactory(); + + // When + CompletionStage channelFuture = + factory.connect( + SERVER_ADDRESS, DriverChannelOptions.DEFAULT, NoopNodeMetricUpdater.INSTANCE); + + writeInboundFrame(readOutboundFrame(), new Ready()); + writeInboundFrame(readOutboundFrame(), TestResponses.clusterNameResponse("mockClusterName")); + + // Then + assertThatStage(channelFuture).isSuccess(); + assertThat(factory.clusterName).isEqualTo("mockClusterName"); + } + + @Test + public void should_check_cluster_name_for_next_connections() throws Throwable { + // Given + when(defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn(false); + when(protocolVersionRegistry.highestNonBeta()).thenReturn(DefaultProtocolVersion.V4); + ChannelFactory factory = newChannelFactory(); + + // When + CompletionStage channelFuture = + factory.connect( + SERVER_ADDRESS, DriverChannelOptions.DEFAULT, NoopNodeMetricUpdater.INSTANCE); + // open a first connection that will define the cluster name + writeInboundFrame(readOutboundFrame(), new Ready()); + writeInboundFrame(readOutboundFrame(), TestResponses.clusterNameResponse("mockClusterName")); + assertThatStage(channelFuture).isSuccess(); + // open a second connection that returns the same cluster name + channelFuture = + factory.connect( + SERVER_ADDRESS, DriverChannelOptions.DEFAULT, NoopNodeMetricUpdater.INSTANCE); + writeInboundFrame(readOutboundFrame(), new Ready()); + writeInboundFrame(readOutboundFrame(), TestResponses.clusterNameResponse("mockClusterName")); + + // Then + assertThatStage(channelFuture).isSuccess(); + + // When + // open a third connection that returns a different cluster name + channelFuture = + factory.connect( + SERVER_ADDRESS, DriverChannelOptions.DEFAULT, NoopNodeMetricUpdater.INSTANCE); + writeInboundFrame(readOutboundFrame(), new Ready()); + writeInboundFrame(readOutboundFrame(), TestResponses.clusterNameResponse("wrongClusterName")); + + // Then + assertThatStage(channelFuture) + .isFailed( + e -> + assertThat(e) + .isInstanceOf(ClusterNameMismatchException.class) + .hasMessageContaining( + "reports cluster name 'wrongClusterName' that doesn't match " + + "our cluster name 'mockClusterName'.")); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryProtocolNegotiationTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryProtocolNegotiationTest.java new file mode 100644 index 00000000000..500c665cdd7 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryProtocolNegotiationTest.java @@ -0,0 +1,206 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.UnsupportedProtocolVersionException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.internal.core.TestResponses; +import com.datastax.oss.driver.internal.core.metrics.NoopNodeMetricUpdater; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.Error; +import com.datastax.oss.protocol.internal.response.Ready; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Optional; +import java.util.concurrent.CompletionStage; +import org.junit.Test; + +public class ChannelFactoryProtocolNegotiationTest extends ChannelFactoryTestBase { + + @Test + public void should_succeed_if_version_specified_and_supported_by_server() { + // Given + when(defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn(true); + when(defaultProfile.getString(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn("V4"); + when(protocolVersionRegistry.fromName("V4")).thenReturn(DefaultProtocolVersion.V4); + ChannelFactory factory = newChannelFactory(); + + // When + CompletionStage channelFuture = + factory.connect( + SERVER_ADDRESS, DriverChannelOptions.DEFAULT, NoopNodeMetricUpdater.INSTANCE); + + completeSimpleChannelInit(); + + // Then + assertThatStage(channelFuture) + .isSuccess(channel -> assertThat(channel.getClusterName()).isEqualTo("mockClusterName")); + assertThat(factory.protocolVersion).isEqualTo(DefaultProtocolVersion.V4); + } + + @Test + @UseDataProvider("unsupportedProtocolCodes") + public void should_fail_if_version_specified_and_not_supported_by_server(int errorCode) { + // Given + when(defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn(true); + when(defaultProfile.getString(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn("V4"); + when(protocolVersionRegistry.fromName("V4")).thenReturn(DefaultProtocolVersion.V4); + ChannelFactory factory = newChannelFactory(); + + // When + CompletionStage channelFuture = + factory.connect( + SERVER_ADDRESS, DriverChannelOptions.DEFAULT, NoopNodeMetricUpdater.INSTANCE); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.protocolVersion).isEqualTo(DefaultProtocolVersion.V4.getCode()); + // Server does not support v4 + writeInboundFrame( + requestFrame, new Error(errorCode, "Invalid or unsupported protocol version")); + + // Then + assertThatStage(channelFuture) + .isFailed( + e -> { + assertThat(e) + .isInstanceOf(UnsupportedProtocolVersionException.class) + .hasMessageContaining("Host does not support protocol version V4"); + assertThat(((UnsupportedProtocolVersionException) e).getAttemptedVersions()) + .containsExactly(DefaultProtocolVersion.V4); + }); + } + + @Test + public void should_succeed_if_version_not_specified_and_server_supports_latest_supported() { + // Given + when(defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn(false); + when(protocolVersionRegistry.highestNonBeta()).thenReturn(DefaultProtocolVersion.V4); + ChannelFactory factory = newChannelFactory(); + + // When + CompletionStage channelFuture = + factory.connect( + SERVER_ADDRESS, DriverChannelOptions.DEFAULT, NoopNodeMetricUpdater.INSTANCE); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.protocolVersion).isEqualTo(DefaultProtocolVersion.V4.getCode()); + writeInboundFrame(requestFrame, new Ready()); + + requestFrame = readOutboundFrame(); + writeInboundFrame(requestFrame, TestResponses.clusterNameResponse("mockClusterName")); + + // Then + assertThatStage(channelFuture) + .isSuccess(channel -> assertThat(channel.getClusterName()).isEqualTo("mockClusterName")); + assertThat(factory.protocolVersion).isEqualTo(DefaultProtocolVersion.V4); + } + + @Test + @UseDataProvider("unsupportedProtocolCodes") + public void should_negotiate_if_version_not_specified_and_server_supports_legacy(int errorCode) { + // Given + when(defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn(false); + when(protocolVersionRegistry.highestNonBeta()).thenReturn(DefaultProtocolVersion.V4); + when(protocolVersionRegistry.downgrade(DefaultProtocolVersion.V4)) + .thenReturn(Optional.of(DefaultProtocolVersion.V3)); + ChannelFactory factory = newChannelFactory(); + + // When + CompletionStage channelFuture = + factory.connect( + SERVER_ADDRESS, DriverChannelOptions.DEFAULT, NoopNodeMetricUpdater.INSTANCE); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.protocolVersion).isEqualTo(DefaultProtocolVersion.V4.getCode()); + // Server does not support v4 + writeInboundFrame( + requestFrame, new Error(errorCode, "Invalid or unsupported protocol version")); + + // Then + // Factory should initialize a new connection, that retries with the lower version + requestFrame = readOutboundFrame(); + assertThat(requestFrame.protocolVersion).isEqualTo(DefaultProtocolVersion.V3.getCode()); + writeInboundFrame(requestFrame, new Ready()); + + requestFrame = readOutboundFrame(); + writeInboundFrame(requestFrame, TestResponses.clusterNameResponse("mockClusterName")); + assertThatStage(channelFuture) + .isSuccess(channel -> assertThat(channel.getClusterName()).isEqualTo("mockClusterName")); + assertThat(factory.protocolVersion).isEqualTo(DefaultProtocolVersion.V3); + } + + @Test + @UseDataProvider("unsupportedProtocolCodes") + public void should_fail_if_negotiation_finds_no_matching_version(int errorCode) { + // Given + when(defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn(false); + when(protocolVersionRegistry.highestNonBeta()).thenReturn(DefaultProtocolVersion.V4); + when(protocolVersionRegistry.downgrade(DefaultProtocolVersion.V4)) + .thenReturn(Optional.of(DefaultProtocolVersion.V3)); + when(protocolVersionRegistry.downgrade(DefaultProtocolVersion.V3)).thenReturn(Optional.empty()); + ChannelFactory factory = newChannelFactory(); + + // When + CompletionStage channelFuture = + factory.connect( + SERVER_ADDRESS, DriverChannelOptions.DEFAULT, NoopNodeMetricUpdater.INSTANCE); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.protocolVersion).isEqualTo(DefaultProtocolVersion.V4.getCode()); + // Server does not support v4 + writeInboundFrame( + requestFrame, new Error(errorCode, "Invalid or unsupported protocol version")); + + // Client retries with v3 + requestFrame = readOutboundFrame(); + assertThat(requestFrame.protocolVersion).isEqualTo(DefaultProtocolVersion.V3.getCode()); + // Server does not support v3 + writeInboundFrame( + requestFrame, new Error(errorCode, "Invalid or unsupported protocol version")); + + // Then + assertThatStage(channelFuture) + .isFailed( + e -> { + assertThat(e) + .isInstanceOf(UnsupportedProtocolVersionException.class) + .hasMessageContaining( + "Protocol negotiation failed: could not find a common version " + + "(attempted: [V4, V3])"); + assertThat(((UnsupportedProtocolVersionException) e).getAttemptedVersions()) + .containsExactly(DefaultProtocolVersion.V4, DefaultProtocolVersion.V3); + }); + } + + /** + * Depending on the Cassandra version, an "unsupported protocol" response can use different error + * codes, so we test all of them. + */ + @DataProvider + public static Object[][] unsupportedProtocolCodes() { + return new Object[][] { + new Object[] {ProtocolConstants.ErrorCode.PROTOCOL_ERROR}, + // C* 2.1 reports a server error instead of protocol error, see CASSANDRA-9451. + new Object[] {ProtocolConstants.ErrorCode.SERVER_ERROR} + }; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java new file mode 100644 index 00000000000..afcb507bfad --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelFactoryTestBase.java @@ -0,0 +1,276 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.internal.core.ProtocolVersionRegistry; +import com.datastax.oss.driver.internal.core.TestResponses; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.metrics.NodeMetricUpdater; +import com.datastax.oss.driver.internal.core.protocol.ByteBufPrimitiveCodec; +import com.datastax.oss.protocol.internal.Compressor; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.FrameCodec; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.response.Ready; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.local.LocalChannel; +import io.netty.channel.local.LocalServerChannel; +import java.time.Duration; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Exchanger; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.After; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; + +/** + * Sets up the infrastructure for channel factory tests. + * + *

Because the factory manages channel creation itself, {@link + * io.netty.channel.embedded.EmbeddedChannel} is not suitable. Instead, we launch an embedded server + * and connect to it with the local transport. + * + *

The current implementation assumes that only one connection will be tested at a time, but + * support for multiple simultaneous connections could easily be added: store multiple instances of + * requestFrameExchanger and serverResponseChannel, and add a parameter to readOutboundFrame and + * writeInboundFrame (for instance the position of the connection in creation order) to specify + * which instance to use. + */ +@RunWith(DataProviderRunner.class) +public abstract class ChannelFactoryTestBase { + static final EndPoint SERVER_ADDRESS = + new LocalEndPoint(ChannelFactoryTestBase.class.getSimpleName() + "-server"); + + private static final int TIMEOUT_MILLIS = 500; + + DefaultEventLoopGroup serverGroup; + DefaultEventLoopGroup clientGroup; + + @Mock InternalDriverContext context; + @Mock DriverConfig driverConfig; + @Mock DriverExecutionProfile defaultProfile; + @Mock NettyOptions nettyOptions; + @Mock ProtocolVersionRegistry protocolVersionRegistry; + @Mock EventBus eventBus; + @Mock Compressor compressor; + + // The server's I/O thread will store the last received request here, and block until the test + // thread retrieves it. This assumes readOutboundFrame() is called for each actual request, else + // the test will hang forever. + private final Exchanger requestFrameExchanger = new Exchanger<>(); + + // The channel that accepts incoming connections on the server + private LocalServerChannel serverAcceptChannel; + // The channel to send responses to the last open connection + private volatile LocalChannel serverResponseChannel; + + @Before + public void setup() throws InterruptedException { + MockitoAnnotations.initMocks(this); + + serverGroup = new DefaultEventLoopGroup(1); + clientGroup = new DefaultEventLoopGroup(1); + + when(context.getConfig()).thenReturn(driverConfig); + when(driverConfig.getDefaultProfile()).thenReturn(defaultProfile); + when(defaultProfile.isDefined(DefaultDriverOption.AUTH_PROVIDER_CLASS)).thenReturn(false); + when(defaultProfile.getDuration(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT)) + .thenReturn(Duration.ofMillis(TIMEOUT_MILLIS)); + when(defaultProfile.getDuration(DefaultDriverOption.CONNECTION_SET_KEYSPACE_TIMEOUT)) + .thenReturn(Duration.ofMillis(TIMEOUT_MILLIS)); + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_MAX_REQUESTS)).thenReturn(1); + when(defaultProfile.getDuration(DefaultDriverOption.HEARTBEAT_INTERVAL)) + .thenReturn(Duration.ofSeconds(30)); + + when(context.getProtocolVersionRegistry()).thenReturn(protocolVersionRegistry); + when(context.getNettyOptions()).thenReturn(nettyOptions); + when(nettyOptions.ioEventLoopGroup()).thenReturn(clientGroup); + when(nettyOptions.channelClass()).thenAnswer((Answer) i -> LocalChannel.class); + when(nettyOptions.allocator()).thenReturn(ByteBufAllocator.DEFAULT); + when(context.getFrameCodec()) + .thenReturn( + FrameCodec.defaultClient( + new ByteBufPrimitiveCodec(ByteBufAllocator.DEFAULT), Compressor.none())); + when(context.getSslHandlerFactory()).thenReturn(Optional.empty()); + when(context.getEventBus()).thenReturn(eventBus); + when(context.getWriteCoalescer()).thenReturn(new PassThroughWriteCoalescer(null)); + when(context.getCompressor()).thenReturn(compressor); + + // Start local server + ServerBootstrap serverBootstrap = + new ServerBootstrap() + .group(serverGroup) + .channel(LocalServerChannel.class) + .localAddress(SERVER_ADDRESS.resolve()) + .childHandler(new ServerInitializer()); + ChannelFuture channelFuture = serverBootstrap.bind().sync(); + serverAcceptChannel = (LocalServerChannel) channelFuture.sync().channel(); + } + + // Sets up the pipeline for our local server + private class ServerInitializer extends ChannelInitializer { + @Override + protected void initChannel(LocalChannel ch) throws Exception { + // Install a single handler that stores received requests, so that the test can check what + // the client sent + ch.pipeline() + .addLast( + new ChannelInboundHandlerAdapter() { + @Override + @SuppressWarnings("unchecked") + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + super.channelRead(ctx, msg); + requestFrameExchanger.exchange((Frame) msg); + } + }); + + // Store the channel so that the test can send responses back to the client + serverResponseChannel = ch; + } + } + + protected Frame readOutboundFrame() { + try { + return requestFrameExchanger.exchange(null, TIMEOUT_MILLIS, MILLISECONDS); + } catch (InterruptedException e) { + fail("unexpected interruption while waiting for outbound frame", e); + } catch (TimeoutException e) { + fail("Timed out reading outbound frame"); + } + return null; // never reached + } + + protected void writeInboundFrame(Frame requestFrame, Message response) { + writeInboundFrame(requestFrame, response, requestFrame.protocolVersion); + } + + private void writeInboundFrame(Frame requestFrame, Message response, int protocolVersion) { + serverResponseChannel.writeAndFlush( + Frame.forResponse( + protocolVersion, + requestFrame.streamId, + null, + Frame.NO_PAYLOAD, + Collections.emptyList(), + response)); + } + + /** + * Simulate the sequence of roundtrips to initialize a simple channel without authentication or + * keyspace (avoids repeating it in subclasses). + */ + protected void completeSimpleChannelInit() { + Frame requestFrame = readOutboundFrame(); + writeInboundFrame(requestFrame, new Ready()); + + requestFrame = readOutboundFrame(); + writeInboundFrame(requestFrame, TestResponses.clusterNameResponse("mockClusterName")); + } + + ChannelFactory newChannelFactory() { + return new TestChannelFactory(context); + } + + // A simplified channel factory to use in the tests. + // It only installs high-level handlers on the pipeline, not the frame codecs. So we'll receive + // Frame objects on the server side, which is simpler to test. + private static class TestChannelFactory extends ChannelFactory { + + private TestChannelFactory(InternalDriverContext internalDriverContext) { + super(internalDriverContext); + } + + @Override + ChannelInitializer initializer( + EndPoint endPoint, + ProtocolVersion protocolVersion, + DriverChannelOptions options, + NodeMetricUpdater nodeMetricUpdater, + CompletableFuture resultFuture) { + return new ChannelInitializer() { + @Override + protected void initChannel(Channel channel) throws Exception { + try { + DriverExecutionProfile defaultProfile = context.getConfig().getDefaultProfile(); + + long setKeyspaceTimeoutMillis = + defaultProfile + .getDuration(DefaultDriverOption.CONNECTION_SET_KEYSPACE_TIMEOUT) + .toMillis(); + int maxRequestsPerConnection = + defaultProfile.getInt(DefaultDriverOption.CONNECTION_MAX_REQUESTS); + + InFlightHandler inFlightHandler = + new InFlightHandler( + protocolVersion, + new StreamIdGenerator(maxRequestsPerConnection), + Integer.MAX_VALUE, + setKeyspaceTimeoutMillis, + channel.newPromise(), + null, + "test"); + + HeartbeatHandler heartbeatHandler = new HeartbeatHandler(defaultProfile); + ProtocolInitHandler initHandler = + new ProtocolInitHandler( + context, protocolVersion, clusterName, endPoint, options, heartbeatHandler); + channel.pipeline().addLast("inflight", inFlightHandler).addLast("init", initHandler); + } catch (Throwable t) { + resultFuture.completeExceptionally(t); + } + } + }; + } + } + + @After + public void tearDown() throws InterruptedException { + serverAcceptChannel.close(); + + serverGroup + .shutdownGracefully(TIMEOUT_MILLIS, TIMEOUT_MILLIS * 2, TimeUnit.MILLISECONDS) + .sync(); + clientGroup + .shutdownGracefully(TIMEOUT_MILLIS, TIMEOUT_MILLIS * 2, TimeUnit.MILLISECONDS) + .sync(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelHandlerTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelHandlerTestBase.java new file mode 100644 index 00000000000..d0da5377c5f --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ChannelHandlerTestBase.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.Message; +import io.netty.channel.embedded.EmbeddedChannel; +import java.util.Collections; +import org.junit.Before; + +/** + * Infrastructure for channel handler test. + * + *

It relies on an embedded channel where the tested handler is installed. Then the test can + * simulate incoming/outgoing messages, and check that the handler propagates the adequate messages + * upstream/downstream. + */ +public class ChannelHandlerTestBase { + protected EmbeddedChannel channel; + + @Before + public void setup() { + channel = new EmbeddedChannel(); + } + + /** Reads a request frame that we expect the tested handler to have sent inbound. */ + protected Frame readInboundFrame() { + channel.runPendingTasks(); + Object o = channel.readInbound(); + assertThat(o).isInstanceOf(Frame.class); + return ((Frame) o); + } + + /** Reads a request frame that we expect the tested handler to have sent outbound. */ + protected Frame readOutboundFrame() { + channel.runPendingTasks(); + Object o = channel.readOutbound(); + assertThat(o).isInstanceOf(Frame.class); + return ((Frame) o); + } + + protected void assertNoOutboundFrame() { + channel.runPendingTasks(); + Object o = channel.readOutbound(); + assertThat(o).isNull(); + } + + /** Writes a response frame for the tested handler to read. */ + protected void writeInboundFrame(Frame responseFrame) { + channel.writeInbound(responseFrame); + } + + /** Writes a response frame that matches the given request, with the given response message. */ + protected void writeInboundFrame(Frame requestFrame, Message response) { + channel.writeInbound(buildInboundFrame(requestFrame, response)); + } + + /** Builds a response frame matching a request frame. */ + protected Frame buildInboundFrame(Frame requestFrame, Message response) { + return Frame.forResponse( + requestFrame.protocolVersion, + requestFrame.streamId, + null, + requestFrame.customPayload, + Collections.emptyList(), + response); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ConnectInitHandlerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ConnectInitHandlerTest.java new file mode 100644 index 00000000000..ba5fd28aeb5 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ConnectInitHandlerTest.java @@ -0,0 +1,122 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import java.net.InetSocketAddress; +import org.junit.Before; +import org.junit.Test; + +public class ConnectInitHandlerTest extends ChannelHandlerTestBase { + + private TestHandler handler; + + @Before + @Override + public void setup() { + super.setup(); + handler = new TestHandler(); + channel.pipeline().addLast(handler); + } + + @Test + public void should_call_onRealConnect_when_connection_succeeds() { + assertThat(handler.hasConnected).isFalse(); + + // When + channel.connect(new InetSocketAddress("localhost", 9042)); + + // Then + assertThat(handler.hasConnected).isTrue(); + } + + @Test + public void should_not_complete_connect_future_before_triggered_by_handler() { + // When + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + // Then + assertThat(connectFuture.isDone()).isFalse(); + } + + @Test + public void should_complete_connect_future_when_handler_completes() { + // Given + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + // When + handler.setConnectSuccess(); + + // Then + assertThat(connectFuture.isSuccess()).isTrue(); + } + + @Test + public void should_remove_handler_from_pipeline_when_handler_completes() { + // Given + channel.connect(new InetSocketAddress("localhost", 9042)); + + // When + handler.setConnectSuccess(); + + // Then + assertThat(channel.pipeline().get(TestHandler.class)).isNull(); + } + + @Test + public void should_fail_connect_future_when_handler_fails() { + // Given + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + Exception exception = new Exception("test"); + + // When + handler.setConnectFailure(exception); + + // Then + assertThat(connectFuture).isFailed(e -> assertThat(e).isEqualTo(exception)); + } + + /** + * Well-behaved implementations should not call setConnect* multiple times in a row, but check + * that we handle it gracefully if they do. + */ + @Test + public void should_ignore_subsequent_calls_if_handler_already_failed() { + // Given + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + Exception exception = new Exception("test"); + + // When + handler.setConnectFailure(exception); + handler.setConnectFailure(new Exception("test2")); + handler.setConnectSuccess(); + + // Then + assertThat(connectFuture).isFailed(e -> assertThat(e).isEqualTo(exception)); + } + + static class TestHandler extends ConnectInitHandler { + boolean hasConnected; + + @Override + protected void onRealConnect(ChannelHandlerContext ctx) { + hasConnected = true; + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/DriverChannelTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/DriverChannelTest.java new file mode 100644 index 00000000000..75ebcab9efa --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/DriverChannelTest.java @@ -0,0 +1,163 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.connection.ClosedConnectionException; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.request.Query; +import com.datastax.oss.protocol.internal.response.result.Void; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelPromise; +import io.netty.util.concurrent.Future; +import java.util.AbstractMap; +import java.util.ArrayDeque; +import java.util.Map; +import java.util.Queue; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class DriverChannelTest extends ChannelHandlerTestBase { + public static final int SET_KEYSPACE_TIMEOUT_MILLIS = 100; + + private DriverChannel driverChannel; + private MockWriteCoalescer writeCoalescer; + + @Mock private StreamIdGenerator streamIds; + + @Before + @Override + public void setup() { + super.setup(); + MockitoAnnotations.initMocks(this); + channel + .pipeline() + .addLast( + new InFlightHandler( + DefaultProtocolVersion.V3, + streamIds, + Integer.MAX_VALUE, + SET_KEYSPACE_TIMEOUT_MILLIS, + channel.newPromise(), + null, + "test")); + writeCoalescer = new MockWriteCoalescer(); + driverChannel = + new DriverChannel( + new EmbeddedEndPoint(channel), channel, writeCoalescer, DefaultProtocolVersion.V3); + } + + /** + * Ensures that the potential delay introduced by the write coalescer does not mess with the + * graceful shutdown sequence: any write submitted before {@link DriverChannel#close()} is + * guaranteed to complete. + */ + @Test + public void should_wait_for_coalesced_writes_when_closing_gracefully() { + // Given + MockResponseCallback responseCallback = new MockResponseCallback(); + driverChannel.write(new Query("test"), false, Frame.NO_PAYLOAD, responseCallback); + // nothing written yet because the coalescer hasn't flushed + assertNoOutboundFrame(); + + // When + Future closeFuture = driverChannel.close(); + + // Then + // not closed yet because there is still a pending write + assertThat(closeFuture).isNotDone(); + assertNoOutboundFrame(); + + // When + // the coalescer finally runs + writeCoalescer.triggerFlush(); + + // Then + // the pending write goes through + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame).isNotNull(); + // not closed yet because there is now a pending response + assertThat(closeFuture).isNotDone(); + + // When + // the pending response arrives + writeInboundFrame(requestFrame, Void.INSTANCE); + assertThat(responseCallback.getLastResponse().message).isEqualTo(Void.INSTANCE); + + // Then + assertThat(closeFuture).isSuccess(); + } + + /** + * Ensures that the potential delay introduced by the write coalescer does not mess with the + * forceful shutdown sequence: any write submitted before {@link DriverChannel#forceClose()} + * should get the "Channel was force-closed" error, whether it had been flushed or not. + */ + @Test + public void should_wait_for_coalesced_writes_when_closing_forcefully() { + // Given + MockResponseCallback responseCallback = new MockResponseCallback(); + driverChannel.write(new Query("test"), false, Frame.NO_PAYLOAD, responseCallback); + // nothing written yet because the coalescer hasn't flushed + assertNoOutboundFrame(); + + // When + Future closeFuture = driverChannel.forceClose(); + + // Then + // not closed yet because there is still a pending write + assertThat(closeFuture).isNotDone(); + assertNoOutboundFrame(); + + // When + // the coalescer finally runs + writeCoalescer.triggerFlush(); + // and the pending write goes through + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame).isNotNull(); + + // Then + assertThat(closeFuture).isSuccess(); + assertThat(responseCallback.getFailure()) + .isInstanceOf(ClosedConnectionException.class) + .hasMessageContaining("Channel was force-closed"); + } + + // Simple implementation that holds all the writes, and flushes them when it's explicitly + // triggered. + private class MockWriteCoalescer implements WriteCoalescer { + private Queue> messages = new ArrayDeque<>(); + + @Override + public ChannelFuture writeAndFlush(Channel channel, Object message) { + assertThat(channel).isEqualTo(DriverChannelTest.this.channel); + ChannelPromise writePromise = channel.newPromise(); + messages.offer(new AbstractMap.SimpleEntry<>(message, writePromise)); + return writePromise; + } + + void triggerFlush() { + for (Map.Entry entry : messages) { + channel.writeAndFlush(entry.getKey(), entry.getValue()); + } + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/EmbeddedEndPoint.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/EmbeddedEndPoint.java new file mode 100644 index 00000000000..53f7c95ad42 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/EmbeddedEndPoint.java @@ -0,0 +1,40 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import io.netty.channel.embedded.EmbeddedChannel; +import java.net.SocketAddress; + +/** Endpoint implementation for unit tests that use an embedded Netty channel. */ +public class EmbeddedEndPoint implements EndPoint { + + private final SocketAddress address; + + public EmbeddedEndPoint(EmbeddedChannel channel) { + this.address = channel.remoteAddress(); + } + + @Override + public SocketAddress resolve() { + throw new UnsupportedOperationException("This should not get called from unit tests"); + } + + @Override + public String asMetricPrefix() { + throw new UnsupportedOperationException("This should not get called from unit tests"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/InFlightHandlerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/InFlightHandlerTest.java new file mode 100644 index 00000000000..7b8c7f870ce --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/InFlightHandlerTest.java @@ -0,0 +1,596 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.connection.BusyConnectionException; +import com.datastax.oss.driver.api.core.connection.ClosedConnectionException; +import com.datastax.oss.driver.internal.core.protocol.FrameDecodingException; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.request.Query; +import com.datastax.oss.protocol.internal.response.Error; +import com.datastax.oss.protocol.internal.response.event.StatusChangeEvent; +import com.datastax.oss.protocol.internal.response.result.SetKeyspace; +import com.datastax.oss.protocol.internal.response.result.Void; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelPromise; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class InFlightHandlerTest extends ChannelHandlerTestBase { + private static final Query QUERY = new Query("select * from foo"); + private static final int SET_KEYSPACE_TIMEOUT_MILLIS = 100; + private static final int MAX_ORPHAN_IDS = 10; + + @Mock private StreamIdGenerator streamIds; + + @Before + @Override + public void setup() { + super.setup(); + MockitoAnnotations.initMocks(this); + } + + @Test + public void should_fail_if_connection_busy() throws Throwable { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(-1); + + // When + ChannelFuture writeFuture = + channel.writeAndFlush( + new DriverChannel.RequestMessage( + QUERY, false, Frame.NO_PAYLOAD, new MockResponseCallback())); + + // Then + assertThat(writeFuture) + .isFailed(e -> assertThat(e).isInstanceOf(BusyConnectionException.class)); + } + + @Test + public void should_assign_streamid_and_send_frame() { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42); + MockResponseCallback responseCallback = new MockResponseCallback(); + + // When + ChannelFuture writeFuture = + channel.writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)); + + // Then + assertThat(writeFuture).isSuccess(); + verify(streamIds).acquire(); + + Frame frame = readOutboundFrame(); + assertThat(frame.streamId).isEqualTo(42); + assertThat(frame.message).isEqualTo(QUERY); + } + + @Test + public void should_notify_callback_of_response() { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42); + MockResponseCallback responseCallback = new MockResponseCallback(); + channel.writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)); + Frame requestFrame = readOutboundFrame(); + + // When + Frame responseFrame = buildInboundFrame(requestFrame, Void.INSTANCE); + writeInboundFrame(responseFrame); + + // Then + assertThat(responseCallback.getLastResponse()).isSameAs(responseFrame); + verify(streamIds).release(42); + } + + @Test + public void should_notify_response_promise_when_decoding_fails() throws Throwable { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42); + MockResponseCallback responseCallback = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)) + .awaitUninterruptibly(); + + // When + RuntimeException mockCause = new RuntimeException("test"); + channel.pipeline().fireExceptionCaught(new FrameDecodingException(42, mockCause)); + + // Then + assertThat(responseCallback.getFailure()).isSameAs(mockCause); + verify(streamIds).release(42); + } + + @Test + public void should_release_stream_id_when_orphaned_callback_receives_response() { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42); + MockResponseCallback responseCallback = new MockResponseCallback(); + channel.writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)); + Frame requestFrame = readOutboundFrame(); + + // When + channel.writeAndFlush(responseCallback); // means cancellation (see DriverChannel#cancel) + Frame responseFrame = buildInboundFrame(requestFrame, Void.INSTANCE); + writeInboundFrame(responseFrame); + + // Then + verify(streamIds).release(42); + // The response is not propagated, because we assume a callback that cancelled managed its own + // termination + assertThat(responseCallback.getLastResponse()).isNull(); + } + + @Test + public void should_delay_graceful_close_and_complete_when_last_pending_completes() { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42); + MockResponseCallback responseCallback = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)) + .awaitUninterruptibly(); + + // When + channel.write(DriverChannel.GRACEFUL_CLOSE_MESSAGE); + + // Then + // not closed yet because there is one pending request + assertThat(channel.closeFuture()).isNotDone(); + + // When + // completing pending request + Frame requestFrame = readOutboundFrame(); + writeInboundFrame(requestFrame, Void.INSTANCE); + + // Then + assertThat(channel.closeFuture()).isSuccess(); + } + + @Test + public void should_delay_graceful_close_and_complete_when_last_pending_cancelled() { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42); + MockResponseCallback responseCallback = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)) + .awaitUninterruptibly(); + + // When + channel.write(DriverChannel.GRACEFUL_CLOSE_MESSAGE); + + // Then + // not closed yet because there is one pending request + assertThat(channel.closeFuture()).isNotDone(); + + // When + // cancelling pending request + channel.write(responseCallback); + + // Then + assertThat(channel.closeFuture()).isSuccess(); + } + + @Test + public void should_graceful_close_immediately_if_no_pending() { + // Given + addToPipeline(); + + // When + channel.write(DriverChannel.GRACEFUL_CLOSE_MESSAGE); + + // Then + assertThat(channel.closeFuture()).isSuccess(); + } + + @Test + public void should_refuse_new_writes_during_graceful_close() { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42); + MockResponseCallback responseCallback = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)) + .awaitUninterruptibly(); + + // When + channel.write(DriverChannel.GRACEFUL_CLOSE_MESSAGE); + + // Then + // not closed yet because there is one pending request + assertThat(channel.closeFuture()).isNotDone(); + // should not allow other write + ChannelFuture otherWriteFuture = + channel.writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)); + assertThat(otherWriteFuture) + .isFailed( + e -> + assertThat(e) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Channel is closing")); + } + + @Test + public void should_close_gracefully_if_orphan_ids_above_max_and_pending_requests() { + // Given + addToPipeline(); + // Generate n orphan ids by writing and cancelling the requests: + for (int i = 0; i < MAX_ORPHAN_IDS; i++) { + when(streamIds.acquire()).thenReturn(i); + MockResponseCallback responseCallback = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)) + .awaitUninterruptibly(); + channel.writeAndFlush(responseCallback).awaitUninterruptibly(); + } + // Generate another request that is pending and not cancelled: + when(streamIds.acquire()).thenReturn(MAX_ORPHAN_IDS); + MockResponseCallback pendingResponseCallback = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage( + QUERY, false, Frame.NO_PAYLOAD, pendingResponseCallback)) + .awaitUninterruptibly(); + + // When + // Generate the n+1th orphan id that makes us go above the threshold + when(streamIds.acquire()).thenReturn(MAX_ORPHAN_IDS + 1); + MockResponseCallback responseCallback = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)) + .awaitUninterruptibly(); + channel.writeAndFlush(responseCallback).awaitUninterruptibly(); + + // Then + // Channel should be closing gracefully. There's no way to observe that from the outside, so + // write another request and check that it's rejected: + assertThat(channel.closeFuture()).isNotDone(); + ChannelFuture otherWriteFuture = + channel.writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)); + assertThat(otherWriteFuture) + .isFailed( + e -> + assertThat(e) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Channel is closing")); + + // When + // Cancel the last pending request + channel.writeAndFlush(pendingResponseCallback).awaitUninterruptibly(); + + // Then + // The graceful shutdown completes + assertThat(channel.closeFuture()).isSuccess(); + } + + @Test + public void should_close_immediately_if_orphan_ids_above_max_and_no_pending_requests() { + // Given + addToPipeline(); + // Generate n orphan ids by writing and cancelling the requests: + for (int i = 0; i < MAX_ORPHAN_IDS; i++) { + when(streamIds.acquire()).thenReturn(i); + MockResponseCallback responseCallback = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)) + .awaitUninterruptibly(); + channel.writeAndFlush(responseCallback).awaitUninterruptibly(); + } + + // When + // Generate the n+1th orphan id that makes us go above the threshold + when(streamIds.acquire()).thenReturn(MAX_ORPHAN_IDS); + MockResponseCallback responseCallback = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)) + .awaitUninterruptibly(); + channel.writeAndFlush(responseCallback).awaitUninterruptibly(); + + // Then + // Channel should close immediately since no active pending requests. + assertThat(channel.closeFuture()).isSuccess(); + } + + @Test + public void should_fail_all_pending_when_force_closed() throws Throwable { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42, 43); + MockResponseCallback responseCallback1 = new MockResponseCallback(); + MockResponseCallback responseCallback2 = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback1)) + .awaitUninterruptibly(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback2)) + .awaitUninterruptibly(); + + // When + channel.write(DriverChannel.FORCEFUL_CLOSE_MESSAGE); + + // Then + assertThat(channel.closeFuture()).isSuccess(); + for (MockResponseCallback callback : ImmutableList.of(responseCallback1, responseCallback2)) { + assertThat(callback.getFailure()) + .isInstanceOf(ClosedConnectionException.class) + .hasMessageContaining("Channel was force-closed"); + } + } + + @Test + public void should_fail_all_pending_and_close_on_unexpected_inbound_exception() throws Throwable { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42, 43); + MockResponseCallback responseCallback1 = new MockResponseCallback(); + MockResponseCallback responseCallback2 = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback1)) + .awaitUninterruptibly(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback2)) + .awaitUninterruptibly(); + + // When + RuntimeException mockException = new RuntimeException("test"); + channel.pipeline().fireExceptionCaught(mockException); + + // Then + assertThat(channel.closeFuture()).isSuccess(); + for (MockResponseCallback callback : ImmutableList.of(responseCallback1, responseCallback2)) { + Throwable failure = callback.getFailure(); + assertThat(failure).isInstanceOf(ClosedConnectionException.class); + assertThat(failure.getCause()).isSameAs(mockException); + } + } + + @Test + public void should_fail_all_pending_if_connection_lost() { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42, 43); + MockResponseCallback responseCallback1 = new MockResponseCallback(); + MockResponseCallback responseCallback2 = new MockResponseCallback(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback1)) + .awaitUninterruptibly(); + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback2)) + .awaitUninterruptibly(); + + // When + channel.pipeline().fireChannelInactive(); + + // Then + for (MockResponseCallback callback : ImmutableList.of(responseCallback1, responseCallback2)) { + assertThat(callback.getFailure()) + .isInstanceOf(ClosedConnectionException.class) + .hasMessageContaining("Lost connection to remote peer"); + } + } + + @Test + public void should_hold_stream_id_for_multi_response_callback() { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42); + MockResponseCallback responseCallback = + new MockResponseCallback(frame -> frame.message instanceof Error); + + // When + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)) + .awaitUninterruptibly(); + + // Then + // notify callback of stream id + assertThat(responseCallback.streamId).isEqualTo(42); + + Frame requestFrame = readOutboundFrame(); + for (int i = 0; i < 5; i++) { + // When + // completing pending request + Frame responseFrame = buildInboundFrame(requestFrame, Void.INSTANCE); + writeInboundFrame(responseFrame); + + // Then + assertThat(responseCallback.getLastResponse()).isSameAs(responseFrame); + // Stream id not released, callback can receive more responses + verify(streamIds, never()).release(42); + } + + // When + // a terminal response comes in + Frame responseFrame = buildInboundFrame(requestFrame, new Error(0, "test")); + writeInboundFrame(responseFrame); + + // Then + verify(streamIds).release(42); + assertThat(responseCallback.getLastResponse()).isSameAs(responseFrame); + + // When + // more responses come in + writeInboundFrame(requestFrame, Void.INSTANCE); + + // Then + // the callback does not get them anymore (this could only be responses to a new request that + // reused the id) + assertThat(responseCallback.getLastResponse()).isNull(); + } + + @Test + public void + should_release_stream_id_when_orphaned_multi_response_callback_receives_last_response() { + // Given + addToPipeline(); + when(streamIds.acquire()).thenReturn(42); + MockResponseCallback responseCallback = + new MockResponseCallback(frame -> frame.message instanceof Error); + + channel + .writeAndFlush( + new DriverChannel.RequestMessage(QUERY, false, Frame.NO_PAYLOAD, responseCallback)) + .awaitUninterruptibly(); + + Frame requestFrame = readOutboundFrame(); + for (int i = 0; i < 5; i++) { + Frame responseFrame = buildInboundFrame(requestFrame, Void.INSTANCE); + writeInboundFrame(responseFrame); + assertThat(responseCallback.getLastResponse()).isSameAs(responseFrame); + verify(streamIds, never()).release(42); + } + + // When + // cancelled mid-flight + channel.writeAndFlush(responseCallback); + + // Then + // subsequent non-final responses are not propagated (we assume the callback completed itself + // already), but do not release the stream id + writeInboundFrame(requestFrame, Void.INSTANCE); + assertThat(responseCallback.getLastResponse()).isNull(); + verify(streamIds, never()).release(42); + + // When + // the terminal response arrives + writeInboundFrame(requestFrame, new Error(0, "test")); + + // Then + // still not propagated but the id is released + assertThat(responseCallback.getLastResponse()).isNull(); + verify(streamIds).release(42); + } + + @Test + public void should_set_keyspace() { + // Given + addToPipeline(); + ChannelPromise setKeyspacePromise = channel.newPromise(); + DriverChannel.SetKeyspaceEvent setKeyspaceEvent = + new DriverChannel.SetKeyspaceEvent(CqlIdentifier.fromCql("ks"), setKeyspacePromise); + + // When + channel.pipeline().fireUserEventTriggered(setKeyspaceEvent); + Frame requestFrame = readOutboundFrame(); + + // Then + assertThat(requestFrame.message).isInstanceOf(Query.class); + writeInboundFrame(requestFrame, new SetKeyspace("ks")); + assertThat(setKeyspacePromise).isSuccess(); + } + + @Test + public void should_fail_to_set_keyspace_if_query_times_out() throws InterruptedException { + // Given + addToPipeline(); + ChannelPromise setKeyspacePromise = channel.newPromise(); + DriverChannel.SetKeyspaceEvent setKeyspaceEvent = + new DriverChannel.SetKeyspaceEvent(CqlIdentifier.fromCql("ks"), setKeyspacePromise); + + // When + channel.pipeline().fireUserEventTriggered(setKeyspaceEvent); + TimeUnit.MILLISECONDS.sleep(SET_KEYSPACE_TIMEOUT_MILLIS * 2); + channel.runPendingTasks(); + + // Then + assertThat(setKeyspacePromise).isFailed(); + } + + @Test + public void should_notify_callback_of_events() { + // Given + EventCallback eventCallback = mock(EventCallback.class); + addToPipelineWithEventCallback(eventCallback); + + // When + StatusChangeEvent event = + new StatusChangeEvent( + ProtocolConstants.StatusChangeType.UP, new InetSocketAddress("127.0.0.1", 9042)); + Frame eventFrame = + Frame.forResponse( + DefaultProtocolVersion.V3.getCode(), + -1, + null, + Collections.emptyMap(), + Collections.emptyList(), + event); + writeInboundFrame(eventFrame); + + // Then + ArgumentCaptor captor = ArgumentCaptor.forClass(StatusChangeEvent.class); + verify(eventCallback).onEvent(captor.capture()); + assertThat(captor.getValue()).isSameAs(event); + } + + private void addToPipeline() { + addToPipelineWithEventCallback(null); + } + + private void addToPipelineWithEventCallback(EventCallback eventCallback) { + channel + .pipeline() + .addLast( + new InFlightHandler( + DefaultProtocolVersion.V3, + streamIds, + MAX_ORPHAN_IDS, + SET_KEYSPACE_TIMEOUT_MILLIS, + channel.newPromise(), + eventCallback, + "test")); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/LocalEndPoint.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/LocalEndPoint.java new file mode 100644 index 00000000000..c98b5979662 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/LocalEndPoint.java @@ -0,0 +1,40 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import io.netty.channel.local.LocalAddress; +import java.net.SocketAddress; + +/** Endpoint implementation for unit tests that use the local Netty transport. */ +public class LocalEndPoint implements EndPoint { + + private final LocalAddress localAddress; + + public LocalEndPoint(String id) { + this.localAddress = new LocalAddress(id); + } + + @Override + public SocketAddress resolve() { + return localAddress; + } + + @Override + public String asMetricPrefix() { + throw new UnsupportedOperationException("This should not get called from unit tests"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/MockAuthenticator.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/MockAuthenticator.java new file mode 100644 index 00000000000..26db7c768b4 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/MockAuthenticator.java @@ -0,0 +1,48 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.driver.api.core.auth.SyncAuthenticator; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; + +/** + * Dummy authenticator for our tests. + * + *

The initial response is hard-coded. When the server asks it to evaluate a challenge, it always + * replies with the same token. When authentication succeeds, the success token is stored for later + * inspection. + */ +public class MockAuthenticator implements SyncAuthenticator { + static final String INITIAL_RESPONSE = "0xcafebabe"; + + volatile String successToken; + + @Override + public ByteBuffer initialResponseSync() { + return Bytes.fromHexString(INITIAL_RESPONSE); + } + + @Override + public ByteBuffer evaluateChallengeSync(ByteBuffer challenge) { + return challenge; + } + + @Override + public void onAuthenticationSuccessSync(ByteBuffer token) { + successToken = Bytes.toHexString(token); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/MockChannelFactoryHelper.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/MockChannelFactoryHelper.java new file mode 100644 index 00000000000..8379585ddf4 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/MockChannelFactoryHelper.java @@ -0,0 +1,180 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.collect.ListMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.MultimapBuilder; +import com.datastax.oss.driver.shaded.guava.common.collect.Sets; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.internal.util.MockUtil; +import org.mockito.stubbing.OngoingStubbing; + +/** + * Helper class to set up and verify a sequence of invocations on a ChannelFactory mock. + * + *

Use the builder at the beginning of the test to stub expected calls. Then call the verify + * methods throughout the test to check that each call has been performed. + * + *

This class handles asynchronous calls to the thread factory, but it must be used from a single + * thread (see {@link #waitForCalls(Node, int)}). + */ +public class MockChannelFactoryHelper { + + private static final int CONNECT_TIMEOUT_MILLIS = 500; + + public static Builder builder(ChannelFactory channelFactory) { + return new Builder(channelFactory); + } + + private final ChannelFactory channelFactory; + private final InOrder inOrder; + // If waitForCalls sees more invocations than expected, the difference is stored here + private final Map previous = new HashMap<>(); + + public MockChannelFactoryHelper(ChannelFactory channelFactory) { + this.channelFactory = channelFactory; + this.inOrder = inOrder(channelFactory); + } + + public void waitForCall(Node node) { + waitForCalls(node, 1); + } + + /** + * Waits for a given number of calls to {@code ChannelFactory.connect()}. + * + *

Because we test asynchronous, non-blocking code, there might already be more calls than + * expected when this method is called. If so, the extra calls are stored and stored and will be + * taken into account next time. + */ + public void waitForCalls(Node node, int expected) { + int fromLastTime = previous.getOrDefault(node, 0); + if (fromLastTime >= expected) { + previous.put(node, fromLastTime - expected); + return; + } + expected -= fromLastTime; + + // Because we test asynchronous, non-blocking code, there might have been already more + // invocations than expected. Use `atLeast` and a captor to find out. + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(DriverChannelOptions.class); + inOrder + .verify(channelFactory, timeout(CONNECT_TIMEOUT_MILLIS).atLeast(expected)) + .connect(eq(node), optionsCaptor.capture()); + int actual = optionsCaptor.getAllValues().size(); + + int extras = actual - expected; + if (extras > 0) { + previous.compute(node, (k, v) -> (v == null) ? extras : v + extras); + } + } + + public void verifyNoMoreCalls() { + inOrder + .verify(channelFactory, timeout(CONNECT_TIMEOUT_MILLIS).times(0)) + .connect(any(Node.class), any(DriverChannelOptions.class)); + + Set counts = Sets.newHashSet(previous.values()); + if (!counts.isEmpty()) { + assertThat(counts).containsExactly(0); + } + } + + public static class Builder { + private final ChannelFactory channelFactory; + private final ListMultimap invocations = + MultimapBuilder.hashKeys().arrayListValues().build(); + + public Builder(ChannelFactory channelFactory) { + assertThat(MockUtil.isMock(channelFactory)).as("expected a mock").isTrue(); + verifyZeroInteractions(channelFactory); + this.channelFactory = channelFactory; + } + + public Builder success(Node node, DriverChannel channel) { + invocations.put(node, channel); + return this; + } + + public Builder failure(Node node, String error) { + invocations.put(node, new Exception(error)); + return this; + } + + public Builder failure(Node node, Throwable error) { + invocations.put(node, error); + return this; + } + + public Builder pending(Node node, CompletableFuture future) { + invocations.put(node, future); + return this; + } + + public MockChannelFactoryHelper build() { + stub(); + return new MockChannelFactoryHelper(channelFactory); + } + + private void stub() { + for (Node node : invocations.keySet()) { + Deque> results = new ArrayDeque<>(); + for (Object object : invocations.get(node)) { + if (object instanceof DriverChannel) { + results.add(CompletableFuture.completedFuture(((DriverChannel) object))); + } else if (object instanceof Throwable) { + results.add(CompletableFutures.failedFuture(((Throwable) object))); + } else if (object instanceof CompletableFuture) { + @SuppressWarnings("unchecked") + CompletionStage future = (CompletionStage) object; + results.add(future); + } else { + fail("unexpected type: " + object.getClass()); + } + } + if (results.size() > 0) { + CompletionStage first = results.poll(); + OngoingStubbing> ongoingStubbing = + when(channelFactory.connect(eq(node), any(DriverChannelOptions.class))) + .thenReturn(first); + for (CompletionStage result : results) { + ongoingStubbing.thenReturn(result); + } + } + } + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/MockResponseCallback.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/MockResponseCallback.java new file mode 100644 index 00000000000..d8f5604ac72 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/MockResponseCallback.java @@ -0,0 +1,64 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import com.datastax.oss.protocol.internal.Frame; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.function.Predicate; + +class MockResponseCallback implements ResponseCallback { + private final Queue responses = new ArrayDeque<>(); + private final Predicate isLastResponse; + + volatile int streamId = -1; + + MockResponseCallback() { + this(f -> true); + } + + MockResponseCallback(Predicate isLastResponse) { + this.isLastResponse = isLastResponse; + } + + @Override + public void onResponse(Frame responseFrame) { + responses.offer(responseFrame); + } + + @Override + public void onFailure(Throwable error) { + responses.offer(error); + } + + @Override + public boolean isLastResponse(Frame responseFrame) { + return isLastResponse.test(responseFrame); + } + + @Override + public void onStreamIdAssigned(int streamId) { + this.streamId = streamId; + } + + Frame getLastResponse() { + return (Frame) responses.poll(); + } + + Throwable getFailure() { + return (Throwable) responses.poll(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java new file mode 100644 index 00000000000..5b134f9bc26 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/ProtocolInitHandlerTest.java @@ -0,0 +1,541 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.InvalidKeyspaceException; +import com.datastax.oss.driver.api.core.auth.AuthProvider; +import com.datastax.oss.driver.api.core.auth.AuthenticationException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.internal.core.CassandraProtocolVersionRegistry; +import com.datastax.oss.driver.internal.core.ProtocolVersionRegistry; +import com.datastax.oss.driver.internal.core.TestResponses; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.TestNodeFactory; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.request.AuthResponse; +import com.datastax.oss.protocol.internal.request.Query; +import com.datastax.oss.protocol.internal.request.Register; +import com.datastax.oss.protocol.internal.request.Startup; +import com.datastax.oss.protocol.internal.response.AuthChallenge; +import com.datastax.oss.protocol.internal.response.AuthSuccess; +import com.datastax.oss.protocol.internal.response.Authenticate; +import com.datastax.oss.protocol.internal.response.Error; +import com.datastax.oss.protocol.internal.response.Ready; +import com.datastax.oss.protocol.internal.response.result.SetKeyspace; +import com.datastax.oss.protocol.internal.util.Bytes; +import io.netty.channel.ChannelFuture; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ProtocolInitHandlerTest extends ChannelHandlerTestBase { + + private static final long QUERY_TIMEOUT_MILLIS = 100L; + // The handled only uses this to call the auth provider and for exception messages, so the actual + // value doesn't matter: + private static final EndPoint END_POINT = TestNodeFactory.newEndPoint(1); + + @Mock private InternalDriverContext internalDriverContext; + @Mock private DriverConfig driverConfig; + @Mock private DriverExecutionProfile defaultProfile; + + private ProtocolVersionRegistry protocolVersionRegistry = + new CassandraProtocolVersionRegistry("test"); + private HeartbeatHandler heartbeatHandler; + + @Before + @Override + public void setup() { + super.setup(); + MockitoAnnotations.initMocks(this); + when(internalDriverContext.getConfig()).thenReturn(driverConfig); + when(driverConfig.getDefaultProfile()).thenReturn(defaultProfile); + when(defaultProfile.getDuration(DefaultDriverOption.CONNECTION_INIT_QUERY_TIMEOUT)) + .thenReturn(Duration.ofMillis(QUERY_TIMEOUT_MILLIS)); + when(defaultProfile.getDuration(DefaultDriverOption.HEARTBEAT_INTERVAL)) + .thenReturn(Duration.ofSeconds(30)); + when(internalDriverContext.getProtocolVersionRegistry()).thenReturn(protocolVersionRegistry); + + channel + .pipeline() + .addLast( + "inflight", + new InFlightHandler( + DefaultProtocolVersion.V4, + new StreamIdGenerator(100), + Integer.MAX_VALUE, + 100, + channel.newPromise(), + null, + "test")); + + heartbeatHandler = new HeartbeatHandler(defaultProfile); + } + + @Test + public void should_initialize() { + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + null, + END_POINT, + DriverChannelOptions.DEFAULT, + heartbeatHandler)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + // It should send a STARTUP message + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Startup.class); + assertThat(connectFuture).isNotDone(); + + // Simulate a READY response + writeInboundFrame(buildInboundFrame(requestFrame, new Ready())); + + // Simulate the cluster name check + requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Query.class); + writeInboundFrame(requestFrame, TestResponses.clusterNameResponse("someClusterName")); + + // Init should complete + assertThat(connectFuture).isSuccess(); + } + + @Test + public void should_add_heartbeat_handler_to_pipeline_on_success() { + ProtocolInitHandler protocolInitHandler = + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + null, + END_POINT, + DriverChannelOptions.DEFAULT, + heartbeatHandler); + + channel.pipeline().addLast("init", protocolInitHandler); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + // heartbeat should initially not be in pipeline + assertThat(channel.pipeline().get("heartbeat")).isNull(); + + // It should send a STARTUP message + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Startup.class); + assertThat(connectFuture).isNotDone(); + + // Simulate a READY response + writeInboundFrame(buildInboundFrame(requestFrame, new Ready())); + + // Simulate the cluster name check + requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Query.class); + writeInboundFrame(requestFrame, TestResponses.clusterNameResponse("someClusterName")); + + // Init should complete + assertThat(connectFuture).isSuccess(); + + // should have added heartbeat handler to pipeline. + assertThat(channel.pipeline().get("heartbeat")).isEqualTo(heartbeatHandler); + // should have removed itself from pipeline. + assertThat(channel.pipeline().last()).isNotEqualTo(protocolInitHandler); + } + + @Test + public void should_fail_to_initialize_if_init_query_times_out() throws InterruptedException { + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + null, + END_POINT, + DriverChannelOptions.DEFAULT, + heartbeatHandler)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + readOutboundFrame(); + + // Simulate a pause longer than the timeout + TimeUnit.MILLISECONDS.sleep(QUERY_TIMEOUT_MILLIS * 2); + channel.runPendingTasks(); + + assertThat(connectFuture).isFailed(); + } + + @Test + public void should_initialize_with_authentication() { + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + null, + END_POINT, + DriverChannelOptions.DEFAULT, + heartbeatHandler)); + + String serverAuthenticator = "mockServerAuthenticator"; + AuthProvider authProvider = mock(AuthProvider.class); + MockAuthenticator authenticator = new MockAuthenticator(); + when(authProvider.newAuthenticator(END_POINT, serverAuthenticator)).thenReturn(authenticator); + when(internalDriverContext.getAuthProvider()).thenReturn(Optional.of(authProvider)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Startup.class); + assertThat(connectFuture).isNotDone(); + + // Simulate a response that says that the server requires authentication + writeInboundFrame(requestFrame, new Authenticate(serverAuthenticator)); + + // The connection should have created an authenticator from the auth provider + verify(authProvider).newAuthenticator(END_POINT, serverAuthenticator); + + // And sent an auth response + requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(AuthResponse.class); + AuthResponse authResponse = (AuthResponse) requestFrame.message; + assertThat(Bytes.toHexString(authResponse.token)).isEqualTo(MockAuthenticator.INITIAL_RESPONSE); + assertThat(connectFuture).isNotDone(); + + // As long as the server sends an auth challenge, the client should reply with another + // auth_response + String mockToken = "0xabcd"; + for (int i = 0; i < 5; i++) { + writeInboundFrame(requestFrame, new AuthChallenge(Bytes.fromHexString(mockToken))); + + requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(AuthResponse.class); + authResponse = (AuthResponse) requestFrame.message; + // Our mock impl happens to send back the same token + assertThat(Bytes.toHexString(authResponse.token)).isEqualTo(mockToken); + assertThat(connectFuture).isNotDone(); + } + + // When the server finally sends back a success message, should proceed to the cluster name + // check and succeed + writeInboundFrame(requestFrame, new AuthSuccess(Bytes.fromHexString(mockToken))); + assertThat(authenticator.successToken).isEqualTo(mockToken); + + requestFrame = readOutboundFrame(); + writeInboundFrame(requestFrame, TestResponses.clusterNameResponse("someClusterName")); + + assertThat(connectFuture).isSuccess(); + } + + @Test + public void should_invoke_auth_provider_when_server_does_not_send_challenge() { + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + null, + END_POINT, + DriverChannelOptions.DEFAULT, + heartbeatHandler)); + + AuthProvider authProvider = mock(AuthProvider.class); + when(internalDriverContext.getAuthProvider()).thenReturn(Optional.of(authProvider)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Startup.class); + + // Simulate a READY response, the provider should be notified + writeInboundFrame(buildInboundFrame(requestFrame, new Ready())); + verify(authProvider).onMissingChallenge(END_POINT); + + // Since our mock does nothing, init should proceed normally + requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Query.class); + writeInboundFrame(requestFrame, TestResponses.clusterNameResponse("someClusterName")); + assertThat(connectFuture).isSuccess(); + } + + @Test + public void should_fail_to_initialize_if_server_sends_auth_error() throws Throwable { + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + null, + END_POINT, + DriverChannelOptions.DEFAULT, + heartbeatHandler)); + + String serverAuthenticator = "mockServerAuthenticator"; + AuthProvider authProvider = mock(AuthProvider.class); + MockAuthenticator authenticator = new MockAuthenticator(); + when(authProvider.newAuthenticator(END_POINT, serverAuthenticator)).thenReturn(authenticator); + when(internalDriverContext.getAuthProvider()).thenReturn(Optional.of(authProvider)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Startup.class); + assertThat(connectFuture).isNotDone(); + + writeInboundFrame(requestFrame, new Authenticate("mockServerAuthenticator")); + + requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(AuthResponse.class); + assertThat(connectFuture).isNotDone(); + + writeInboundFrame( + requestFrame, new Error(ProtocolConstants.ErrorCode.AUTH_ERROR, "mock error")); + + assertThat(connectFuture) + .isFailed( + e -> + assertThat(e) + .isInstanceOf(AuthenticationException.class) + .hasMessage( + String.format( + "Authentication error on node %s: server replied 'mock error'", + END_POINT))); + } + + @Test + public void should_check_cluster_name_if_provided() { + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + "expectedClusterName", + END_POINT, + DriverChannelOptions.DEFAULT, + heartbeatHandler)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + Frame requestFrame = readOutboundFrame(); + writeInboundFrame(requestFrame, new Ready()); + + requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Query.class); + Query query = (Query) requestFrame.message; + assertThat(query.query).isEqualTo("SELECT cluster_name FROM system.local"); + assertThat(connectFuture).isNotDone(); + + writeInboundFrame(requestFrame, TestResponses.clusterNameResponse("expectedClusterName")); + + assertThat(connectFuture).isSuccess(); + } + + @Test + public void should_fail_to_initialize_if_cluster_name_does_not_match() throws Throwable { + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + "expectedClusterName", + END_POINT, + DriverChannelOptions.DEFAULT, + heartbeatHandler)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + writeInboundFrame(readOutboundFrame(), new Ready()); + writeInboundFrame( + readOutboundFrame(), TestResponses.clusterNameResponse("differentClusterName")); + + assertThat(connectFuture) + .isFailed( + e -> + assertThat(e) + .isInstanceOf(ClusterNameMismatchException.class) + .hasMessageContaining( + String.format( + "Node %s reports cluster name 'differentClusterName' that doesn't match our cluster name 'expectedClusterName'.", + END_POINT))); + } + + @Test + public void should_initialize_with_keyspace() { + DriverChannelOptions options = + DriverChannelOptions.builder().withKeyspace(CqlIdentifier.fromCql("ks")).build(); + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + null, + END_POINT, + options, + heartbeatHandler)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + writeInboundFrame(readOutboundFrame(), new Ready()); + writeInboundFrame(readOutboundFrame(), TestResponses.clusterNameResponse("someClusterName")); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Query.class); + assertThat(((Query) requestFrame.message).query).isEqualTo("USE \"ks\""); + writeInboundFrame(requestFrame, new SetKeyspace("ks")); + + assertThat(connectFuture).isSuccess(); + } + + @Test + public void should_initialize_with_events() { + List eventTypes = ImmutableList.of("foo", "bar"); + EventCallback eventCallback = mock(EventCallback.class); + DriverChannelOptions driverChannelOptions = + DriverChannelOptions.builder().withEvents(eventTypes, eventCallback).build(); + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + null, + END_POINT, + driverChannelOptions, + heartbeatHandler)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + writeInboundFrame(readOutboundFrame(), new Ready()); + writeInboundFrame(readOutboundFrame(), TestResponses.clusterNameResponse("someClusterName")); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Register.class); + assertThat(((Register) requestFrame.message).eventTypes).containsExactly("foo", "bar"); + writeInboundFrame(requestFrame, new Ready()); + + assertThat(connectFuture).isSuccess(); + } + + @Test + public void should_initialize_with_keyspace_and_events() { + List eventTypes = ImmutableList.of("foo", "bar"); + EventCallback eventCallback = mock(EventCallback.class); + DriverChannelOptions driverChannelOptions = + DriverChannelOptions.builder() + .withKeyspace(CqlIdentifier.fromCql("ks")) + .withEvents(eventTypes, eventCallback) + .build(); + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + null, + END_POINT, + driverChannelOptions, + heartbeatHandler)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + writeInboundFrame(readOutboundFrame(), new Ready()); + writeInboundFrame(readOutboundFrame(), TestResponses.clusterNameResponse("someClusterName")); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Query.class); + assertThat(((Query) requestFrame.message).query).isEqualTo("USE \"ks\""); + writeInboundFrame(requestFrame, new SetKeyspace("ks")); + + requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Register.class); + assertThat(((Register) requestFrame.message).eventTypes).containsExactly("foo", "bar"); + writeInboundFrame(requestFrame, new Ready()); + + assertThat(connectFuture).isSuccess(); + } + + @Test + public void should_fail_to_initialize_if_keyspace_is_invalid() { + DriverChannelOptions driverChannelOptions = + DriverChannelOptions.builder().withKeyspace(CqlIdentifier.fromCql("ks")).build(); + channel + .pipeline() + .addLast( + "init", + new ProtocolInitHandler( + internalDriverContext, + DefaultProtocolVersion.V4, + null, + END_POINT, + driverChannelOptions, + heartbeatHandler)); + + ChannelFuture connectFuture = channel.connect(new InetSocketAddress("localhost", 9042)); + + writeInboundFrame(readOutboundFrame(), new Ready()); + writeInboundFrame(readOutboundFrame(), TestResponses.clusterNameResponse("someClusterName")); + + Frame requestFrame = readOutboundFrame(); + assertThat(requestFrame.message).isInstanceOf(Query.class); + assertThat(((Query) requestFrame.message).query).isEqualTo("USE \"ks\""); + writeInboundFrame( + requestFrame, new Error(ProtocolConstants.ErrorCode.INVALID, "invalid keyspace")); + + assertThat(connectFuture) + .isFailed( + error -> + assertThat(error) + .isInstanceOf(InvalidKeyspaceException.class) + .hasMessage("invalid keyspace")); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/channel/StreamIdGeneratorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/StreamIdGeneratorTest.java new file mode 100644 index 00000000000..7bbbf23c329 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/channel/StreamIdGeneratorTest.java @@ -0,0 +1,61 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.channel; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import org.junit.Test; + +public class StreamIdGeneratorTest { + @Test + public void should_have_all_available_upon_creation() { + StreamIdGenerator generator = new StreamIdGenerator(8); + assertThat(generator.getAvailableIds()).isEqualTo(8); + } + + @Test + public void should_return_available_ids_in_sequence() { + StreamIdGenerator generator = new StreamIdGenerator(8); + for (int i = 0; i < 8; i++) { + assertThat(generator.acquire()).isEqualTo(i); + assertThat(generator.getAvailableIds()).isEqualTo(7 - i); + } + } + + @Test + public void should_return_minus_one_when_no_id_available() { + StreamIdGenerator generator = new StreamIdGenerator(8); + for (int i = 0; i < 8; i++) { + generator.acquire(); + } + assertThat(generator.getAvailableIds()).isEqualTo(0); + assertThat(generator.acquire()).isEqualTo(-1); + } + + @Test + public void should_return_previously_released_ids() { + StreamIdGenerator generator = new StreamIdGenerator(8); + for (int i = 0; i < 8; i++) { + generator.acquire(); + } + generator.release(7); + generator.release(2); + assertThat(generator.getAvailableIds()).isEqualTo(2); + assertThat(generator.acquire()).isEqualTo(2); + assertThat(generator.acquire()).isEqualTo(7); + assertThat(generator.acquire()).isEqualTo(-1); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/DefaultDriverConfigLoaderTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/DefaultDriverConfigLoaderTest.java new file mode 100644 index 00000000000..36d0ad4bb43 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/DefaultDriverConfigLoaderTest.java @@ -0,0 +1,177 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.config.typesafe; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.internal.core.config.ConfigChangeEvent; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.util.concurrent.ScheduledTaskCapturingEventLoop; +import com.datastax.oss.driver.internal.core.util.concurrent.ScheduledTaskCapturingEventLoop.CapturedTask; +import com.typesafe.config.ConfigFactory; +import io.netty.channel.EventLoopGroup; +import java.time.Duration; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class DefaultDriverConfigLoaderTest { + + @Mock private InternalDriverContext context; + @Mock private NettyOptions nettyOptions; + @Mock private EventLoopGroup adminEventExecutorGroup; + @Mock private DriverConfig config; + @Mock private DriverExecutionProfile defaultProfile; + private ScheduledTaskCapturingEventLoop adminExecutor; + private EventBus eventBus; + private AtomicReference configSource; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(context.getSessionName()).thenReturn("test"); + when(context.getNettyOptions()).thenReturn(nettyOptions); + when(nettyOptions.adminEventExecutorGroup()).thenReturn(adminEventExecutorGroup); + + adminExecutor = new ScheduledTaskCapturingEventLoop(adminEventExecutorGroup); + when(adminEventExecutorGroup.next()).thenReturn(adminExecutor); + + eventBus = spy(new EventBus("test")); + when(context.getEventBus()).thenReturn(eventBus); + + // The already loaded config in the context. + // In real life, it's the object managed by the loader, but in this test it's simpler to mock + // it. + when(context.getConfig()).thenReturn(config); + when(config.getDefaultProfile()).thenReturn(defaultProfile); + when(defaultProfile.getDuration(DefaultDriverOption.CONFIG_RELOAD_INTERVAL)) + .thenReturn(Duration.ofSeconds(12)); + + configSource = new AtomicReference<>("int1 = 42"); + } + + @Test + public void should_build_initial_config() { + DefaultDriverConfigLoader loader = + new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get())); + DriverConfig initialConfig = loader.getInitialConfig(); + assertThat(initialConfig).hasIntOption(MockOptions.INT1, 42); + } + + @Test + public void should_schedule_reloading_task() { + DefaultDriverConfigLoader loader = + new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get())); + + loader.onDriverInit(context); + adminExecutor.waitForNonScheduledTasks(); + + CapturedTask task = adminExecutor.nextTask(); + assertThat(task.getInitialDelay(TimeUnit.SECONDS)).isEqualTo(12); + assertThat(task.getPeriod(TimeUnit.SECONDS)).isEqualTo(12); + } + + @Test + public void should_detect_config_change_from_periodic_reload() { + DefaultDriverConfigLoader loader = + new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get())); + DriverConfig initialConfig = loader.getInitialConfig(); + assertThat(initialConfig).hasIntOption(MockOptions.INT1, 42); + + loader.onDriverInit(context); + adminExecutor.waitForNonScheduledTasks(); + + CapturedTask task = adminExecutor.nextTask(); + + configSource.set("int1 = 43"); + + task.run(); + + assertThat(initialConfig).hasIntOption(MockOptions.INT1, 43); + verify(eventBus).fire(ConfigChangeEvent.INSTANCE); + } + + @Test + public void should_detect_config_change_from_manual_reload() { + DefaultDriverConfigLoader loader = + new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get())); + DriverConfig initialConfig = loader.getInitialConfig(); + assertThat(initialConfig).hasIntOption(MockOptions.INT1, 42); + + loader.onDriverInit(context); + adminExecutor.waitForNonScheduledTasks(); + + configSource.set("int1 = 43"); + + CompletionStage reloaded = loader.reload(); + adminExecutor.waitForNonScheduledTasks(); + + assertThat(initialConfig).hasIntOption(MockOptions.INT1, 43); + verify(eventBus).fire(ConfigChangeEvent.INSTANCE); + assertThatStage(reloaded).isSuccess(changed -> assertThat(changed).isTrue()); + } + + @Test + public void should_not_notify_from_periodic_reload_if_config_has_not_changed() { + DefaultDriverConfigLoader loader = + new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get())); + DriverConfig initialConfig = loader.getInitialConfig(); + assertThat(initialConfig).hasIntOption(MockOptions.INT1, 42); + + loader.onDriverInit(context); + adminExecutor.waitForNonScheduledTasks(); + + CapturedTask task = adminExecutor.nextTask(); + + // no change to the config source + + task.run(); + + verify(eventBus, never()).fire(ConfigChangeEvent.INSTANCE); + } + + @Test + public void should_not_notify_from_manual_reload_if_config_has_not_changed() { + DefaultDriverConfigLoader loader = + new DefaultDriverConfigLoader(() -> ConfigFactory.parseString(configSource.get())); + DriverConfig initialConfig = loader.getInitialConfig(); + assertThat(initialConfig).hasIntOption(MockOptions.INT1, 42); + + loader.onDriverInit(context); + adminExecutor.waitForNonScheduledTasks(); + + CompletionStage reloaded = loader.reload(); + adminExecutor.waitForNonScheduledTasks(); + + verify(eventBus, never()).fire(ConfigChangeEvent.INSTANCE); + assertThatStage(reloaded).isSuccess(changed -> assertThat(changed).isFalse()); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/MockOptions.java b/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/MockOptions.java new file mode 100644 index 00000000000..c6870f40802 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/MockOptions.java @@ -0,0 +1,38 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.config.typesafe; + +import com.datastax.oss.driver.api.core.config.DriverOption; +import edu.umd.cs.findbugs.annotations.NonNull; + +enum MockOptions implements DriverOption { + INT1("int1"), + INT2("int2"), + AUTH_PROVIDER("auth_provider"), + ; + + private final String path; + + MockOptions(String path) { + this.path = path; + } + + @NonNull + @Override + public String getPath() { + return path; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverConfigTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverConfigTest.java new file mode 100644 index 00000000000..32889e24afb --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/config/typesafe/TypesafeDriverConfigTest.java @@ -0,0 +1,181 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.config.typesafe; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import java.util.HashMap; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class TypesafeDriverConfigTest { + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void should_load_minimal_config_with_no_profiles() { + TypesafeDriverConfig config = parse("int1 = 42"); + assertThat(config).hasIntOption(MockOptions.INT1, 42); + } + + @Test + public void should_load_config_with_no_profiles_and_optional_values() { + TypesafeDriverConfig config = parse("int1 = 42\n int2 = 43"); + assertThat(config).hasIntOption(MockOptions.INT1, 42); + assertThat(config).hasIntOption(MockOptions.INT2, 43); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_if_profile_uses_default_name() { + parse("int1 = 42\n profiles { default { int1 = 43 } }"); + } + + @Test + public void should_inherit_option_in_profile() { + TypesafeDriverConfig config = parse("int1 = 42\n profiles { profile1 { } }"); + assertThat(config) + .hasIntOption(MockOptions.INT1, 42) + .hasIntOption("profile1", MockOptions.INT1, 42); + } + + @Test + public void should_override_option_in_profile() { + TypesafeDriverConfig config = parse("int1 = 42\n profiles { profile1 { int1 = 43 } }"); + assertThat(config) + .hasIntOption(MockOptions.INT1, 42) + .hasIntOption("profile1", MockOptions.INT1, 43); + } + + @Test + public void should_create_derived_profile_with_new_option() { + TypesafeDriverConfig config = parse("int1 = 42"); + DriverExecutionProfile base = config.getDefaultProfile(); + DriverExecutionProfile derived = base.withInt(MockOptions.INT2, 43); + + assertThat(base.isDefined(MockOptions.INT2)).isFalse(); + assertThat(derived.isDefined(MockOptions.INT2)).isTrue(); + assertThat(derived.getInt(MockOptions.INT2)).isEqualTo(43); + } + + @Test + public void should_create_derived_profile_overriding_option() { + TypesafeDriverConfig config = parse("int1 = 42"); + DriverExecutionProfile base = config.getDefaultProfile(); + DriverExecutionProfile derived = base.withInt(MockOptions.INT1, 43); + + assertThat(base.getInt(MockOptions.INT1)).isEqualTo(42); + assertThat(derived.getInt(MockOptions.INT1)).isEqualTo(43); + } + + @Test + public void should_create_derived_profile_unsetting_option() { + TypesafeDriverConfig config = parse("int1 = 42\n int2 = 43"); + DriverExecutionProfile base = config.getDefaultProfile(); + DriverExecutionProfile derived = base.without(MockOptions.INT2); + + assertThat(base.getInt(MockOptions.INT2)).isEqualTo(43); + assertThat(derived.isDefined(MockOptions.INT2)).isFalse(); + } + + @Test + public void should_fetch_string_map() { + TypesafeDriverConfig config = + parse( + "int1 = 42 \n auth_provider { auth_thing_one= one \n auth_thing_two = two \n auth_thing_three = three}"); + DriverExecutionProfile base = config.getDefaultProfile(); + base.getStringMap(MockOptions.AUTH_PROVIDER); + Map map = base.getStringMap(MockOptions.AUTH_PROVIDER); + assertThat(map.entrySet().size()).isEqualTo(3); + assertThat(map.get("auth_thing_one")).isEqualTo("one"); + assertThat(map.get("auth_thing_two")).isEqualTo("two"); + assertThat(map.get("auth_thing_three")).isEqualTo("three"); + } + + @Test + public void should_create_derived_profile_with_string_map() { + TypesafeDriverConfig config = parse("int1 = 42"); + Map authThingMap = new HashMap<>(); + authThingMap.put("auth_thing_one", "one"); + authThingMap.put("auth_thing_two", "two"); + authThingMap.put("auth_thing_three", "three"); + DriverExecutionProfile base = config.getDefaultProfile(); + DriverExecutionProfile mapBase = base.withStringMap(MockOptions.AUTH_PROVIDER, authThingMap); + Map fetchedMap = mapBase.getStringMap(MockOptions.AUTH_PROVIDER); + assertThat(fetchedMap).isEqualTo(authThingMap); + } + + @Test + public void should_reload() { + TypesafeDriverConfig config = parse("int1 = 42\n profiles { profile1 { int1 = 43 } }"); + + config.reload(ConfigFactory.parseString("int1 = 44\n profiles { profile1 { int1 = 45 } }")); + assertThat(config) + .hasIntOption(MockOptions.INT1, 44) + .hasIntOption("profile1", MockOptions.INT1, 45); + } + + @Test + public void should_update_derived_profiles_after_reloading() { + TypesafeDriverConfig config = parse("int1 = 42\n profiles { profile1 { int1 = 43 } }"); + + DriverExecutionProfile derivedFromDefault = + config.getDefaultProfile().withInt(MockOptions.INT2, 50); + DriverExecutionProfile derivedFromProfile1 = + config.getProfile("profile1").withInt(MockOptions.INT2, 51); + + config.reload(ConfigFactory.parseString("int1 = 44\n profiles { profile1 { int1 = 45 } }")); + + assertThat(derivedFromDefault.getInt(MockOptions.INT1)).isEqualTo(44); + assertThat(derivedFromDefault.getInt(MockOptions.INT2)).isEqualTo(50); + + assertThat(derivedFromProfile1.getInt(MockOptions.INT1)).isEqualTo(45); + assertThat(derivedFromProfile1.getInt(MockOptions.INT2)).isEqualTo(51); + } + + @Test + public void should_enumerate_options() { + TypesafeDriverConfig config = + parse( + "int1 = 42 \n" + + "auth_provider { auth_thing_one= one \n auth_thing_two = two \n auth_thing_three = three}\n" + + "profiles { profile1 { int1 = 45 } }"); + + assertThat(config.getDefaultProfile().entrySet()) + .containsExactly( + entry("auth_provider.auth_thing_one", "one"), + entry("auth_provider.auth_thing_three", "three"), + entry("auth_provider.auth_thing_two", "two"), + entry("int1", 42)); + + assertThat(config.getProfile("profile1").entrySet()) + .containsExactly( + entry("auth_provider.auth_thing_one", "one"), + entry("auth_provider.auth_thing_three", "three"), + entry("auth_provider.auth_thing_two", "two"), + entry("int1", 45)); + } + + private TypesafeDriverConfig parse(String configString) { + Config config = ConfigFactory.parseString(configString); + return new TypesafeDriverConfig(config); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/connection/ExponentialReconnectionPolicyTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/connection/ExponentialReconnectionPolicyTest.java new file mode 100644 index 00000000000..25454a3d76b --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/connection/ExponentialReconnectionPolicyTest.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.connection; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.context.DriverContext; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ExponentialReconnectionPolicyTest { + + @Mock private DriverContext driverContext; + @Mock private DriverConfig driverConfig; + @Mock private DriverExecutionProfile profile; + private final long baseDelay = 1000L; + private final long maxDelay = 60000L; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(driverConfig.getDefaultProfile()).thenReturn(profile); + when(driverContext.getConfig()).thenReturn(driverConfig); + when(profile.getDuration(DefaultDriverOption.RECONNECTION_BASE_DELAY)) + .thenReturn(Duration.of(baseDelay, ChronoUnit.MILLIS)); + when(profile.getDuration(DefaultDriverOption.RECONNECTION_MAX_DELAY)) + .thenReturn(Duration.of(maxDelay, ChronoUnit.MILLIS)); + } + + @Test + public void should_generate_exponential_delay_with_jitter() throws Exception { + ExponentialReconnectionPolicy policy = new ExponentialReconnectionPolicy(driverContext); + ReconnectionPolicy.ReconnectionSchedule schedule = policy.newControlConnectionSchedule(false); + // generate a number of delays and make sure they are all within the base/max values range + for (int i = 0; i < 128; ++i) { + // compute the min and max delays based on attempt count (i) + long exponentialDelay = Math.min(baseDelay * (1L << i), maxDelay); + // min will be 85% of the pure exponential delay (with a floor of baseDelay) + long minJitterDelay = Math.min(baseDelay, (exponentialDelay * 85) / 100); + // max will be 115% of the pure exponential delay (with a ceiling of maxDelay) + long maxJitterDelay = Math.max(maxDelay, (exponentialDelay * 115) / 100); + long delay = schedule.nextDelay().toMillis(); + assertThat(delay).isBetween(minJitterDelay, maxJitterDelay); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/context/StartupOptionsBuilderTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/context/StartupOptionsBuilderTest.java new file mode 100644 index 00000000000..21eea2aa331 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/context/StartupOptionsBuilderTest.java @@ -0,0 +1,108 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.context; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.driver.shaded.guava.common.collect.Maps; +import com.datastax.oss.protocol.internal.request.Startup; +import java.util.List; +import java.util.Map; +import java.util.function.Predicate; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class StartupOptionsBuilderTest { + + private DefaultDriverContext defaultDriverContext; + + // Mocks for instantiating the default driver context + @Mock private DriverConfigLoader configLoader; + private List> typeCodecs = Lists.newArrayList(); + @Mock private NodeStateListener nodeStateListener; + @Mock private SchemaChangeListener schemaChangeListener; + @Mock private RequestTracker requestTracker; + private Map localDatacenters = Maps.newHashMap(); + private Map> nodeFilters = Maps.newHashMap(); + @Mock private ClassLoader classLoader; + @Mock private DriverConfig driverConfig; + @Mock private DriverExecutionProfile defaultProfile; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + when(configLoader.getInitialConfig()).thenReturn(driverConfig); + when(driverConfig.getDefaultProfile()).thenReturn(defaultProfile); + } + + private void buildDriverContext() { + defaultDriverContext = + new DefaultDriverContext( + configLoader, + typeCodecs, + nodeStateListener, + schemaChangeListener, + requestTracker, + localDatacenters, + nodeFilters, + classLoader); + } + + private void assertDefaultStartupOptions(Startup startup) { + assertThat(startup.options).containsEntry(Startup.CQL_VERSION_KEY, "3.0.0"); + assertThat(startup.options) + .containsEntry( + StartupOptionsBuilder.DRIVER_NAME_KEY, Session.OSS_DRIVER_COORDINATES.getName()); + assertThat(startup.options).containsKey(StartupOptionsBuilder.DRIVER_VERSION_KEY); + Version version = Version.parse(startup.options.get(StartupOptionsBuilder.DRIVER_VERSION_KEY)); + assertThat(version).isEqualByComparingTo(Session.OSS_DRIVER_COORDINATES.getVersion()); + } + + @Test + public void should_build_minimal_startup_options() { + buildDriverContext(); + Startup startup = new Startup(defaultDriverContext.getStartupOptions()); + assertThat(startup.options).doesNotContainKey(Startup.COMPRESSION_KEY); + assertDefaultStartupOptions(startup); + } + + @Test + public void should_build_startup_options_with_compression() { + when(defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_COMPRESSION)) + .thenReturn(Boolean.TRUE); + when(defaultProfile.getString(DefaultDriverOption.PROTOCOL_COMPRESSION)).thenReturn("lz4"); + buildDriverContext(); + Startup startup = new Startup(defaultDriverContext.getStartupOptions()); + // assert the compression option is present + assertThat(startup.options).containsEntry(Startup.COMPRESSION_KEY, "lz4"); + assertDefaultStartupOptions(startup); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/context/bus/EventBusTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/context/bus/EventBusTest.java new file mode 100644 index 00000000000..1bf71a7ce6c --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/context/bus/EventBusTest.java @@ -0,0 +1,92 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.context.bus; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.internal.core.context.EventBus; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; + +public class EventBusTest { + + private EventBus bus; + private Map results; + private ChildEvent event = new ChildEvent(); + + @Before + public void setup() { + bus = new EventBus("test"); + results = new HashMap<>(); + } + + @Test + public void should_notify_registered_listeners() { + // Given + bus.register(ChildEvent.class, (e) -> results.put("listener1", e)); + bus.register(ChildEvent.class, (e) -> results.put("listener2", e)); + + // When + bus.fire(event); + + // Then + assertThat(results) + .hasSize(2) + .containsEntry("listener1", event) + .containsEntry("listener2", event); + } + + @Test + public void should_unregister_listener() { + // Given + Object key1 = bus.register(ChildEvent.class, (e) -> results.put("listener1", e)); + bus.register(ChildEvent.class, (e) -> results.put("listener2", e)); + bus.unregister(key1, ChildEvent.class); + + // When + bus.fire(event); + + // Then + assertThat(results).hasSize(1).containsEntry("listener2", event); + } + + @Test + public void should_use_exact_class() { + // Given + bus.register(ChildEvent.class, (e) -> results.put("listener1", e)); + bus.register(ParentEvent.class, (e) -> results.put("listener2", e)); + + // When + bus.fire(event); + + // Then + assertThat(results).hasSize(1).containsEntry("listener1", event); + + // When + results.clear(); + ParentEvent parentEvent = new ParentEvent(); + bus.fire(parentEvent); + + // Then + assertThat(results).hasSize(1).containsEntry("listener2", parentEvent); + } + + private static class ParentEvent {} + + private static class ChildEvent extends ParentEvent {} +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/control/ControlConnectionEventsTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/control/ControlConnectionEventsTest.java new file mode 100644 index 00000000000..7aaebe73b68 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/control/ControlConnectionEventsTest.java @@ -0,0 +1,148 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.control; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.DriverChannelOptions; +import com.datastax.oss.driver.internal.core.channel.EventCallback; +import com.datastax.oss.driver.internal.core.metadata.TopologyEvent; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.event.SchemaChangeEvent; +import com.datastax.oss.protocol.internal.response.event.StatusChangeEvent; +import com.datastax.oss.protocol.internal.response.event.TopologyChangeEvent; +import java.util.concurrent.CompletableFuture; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class ControlConnectionEventsTest extends ControlConnectionTestBase { + + @Test + public void should_register_for_all_events_if_topology_requested() { + // Given + DriverChannel channel1 = newMockDriverChannel(1); + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(DriverChannelOptions.class); + when(channelFactory.connect(eq(node1), optionsCaptor.capture())) + .thenReturn(CompletableFuture.completedFuture(channel1)); + + // When + controlConnection.init(true, false, false); + waitForPendingAdminTasks(); + DriverChannelOptions channelOptions = optionsCaptor.getValue(); + + // Then + assertThat(channelOptions.eventTypes) + .containsExactly( + ProtocolConstants.EventType.SCHEMA_CHANGE, + ProtocolConstants.EventType.STATUS_CHANGE, + ProtocolConstants.EventType.TOPOLOGY_CHANGE); + assertThat(channelOptions.eventCallback).isEqualTo(controlConnection); + } + + @Test + public void should_register_for_schema_events_only_if_topology_not_requested() { + // Given + DriverChannel channel1 = newMockDriverChannel(1); + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(DriverChannelOptions.class); + when(channelFactory.connect(eq(node1), optionsCaptor.capture())) + .thenReturn(CompletableFuture.completedFuture(channel1)); + + // When + controlConnection.init(false, false, false); + waitForPendingAdminTasks(); + DriverChannelOptions channelOptions = optionsCaptor.getValue(); + + // Then + assertThat(channelOptions.eventTypes) + .containsExactly(ProtocolConstants.EventType.SCHEMA_CHANGE); + assertThat(channelOptions.eventCallback).isEqualTo(controlConnection); + } + + @Test + public void should_process_status_change_events() { + // Given + DriverChannel channel1 = newMockDriverChannel(1); + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(DriverChannelOptions.class); + when(channelFactory.connect(eq(node1), optionsCaptor.capture())) + .thenReturn(CompletableFuture.completedFuture(channel1)); + controlConnection.init(true, false, false); + waitForPendingAdminTasks(); + EventCallback callback = optionsCaptor.getValue().eventCallback; + StatusChangeEvent event = + new StatusChangeEvent(ProtocolConstants.StatusChangeType.UP, ADDRESS1); + + // When + callback.onEvent(event); + + // Then + verify(eventBus).fire(TopologyEvent.suggestUp(ADDRESS1)); + } + + @Test + public void should_process_topology_change_events() { + // Given + DriverChannel channel1 = newMockDriverChannel(1); + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(DriverChannelOptions.class); + when(channelFactory.connect(eq(node1), optionsCaptor.capture())) + .thenReturn(CompletableFuture.completedFuture(channel1)); + controlConnection.init(true, false, false); + waitForPendingAdminTasks(); + EventCallback callback = optionsCaptor.getValue().eventCallback; + TopologyChangeEvent event = + new TopologyChangeEvent(ProtocolConstants.TopologyChangeType.NEW_NODE, ADDRESS1); + + // When + callback.onEvent(event); + + // Then + verify(eventBus).fire(TopologyEvent.suggestAdded(ADDRESS1)); + } + + @Test + public void should_process_schema_change_events() { + // Given + DriverChannel channel1 = newMockDriverChannel(1); + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(DriverChannelOptions.class); + when(channelFactory.connect(eq(node1), optionsCaptor.capture())) + .thenReturn(CompletableFuture.completedFuture(channel1)); + controlConnection.init(false, false, false); + waitForPendingAdminTasks(); + EventCallback callback = optionsCaptor.getValue().eventCallback; + SchemaChangeEvent event = + new SchemaChangeEvent( + ProtocolConstants.SchemaChangeType.CREATED, + ProtocolConstants.SchemaChangeTarget.FUNCTION, + "ks", + "fn", + ImmutableList.of("text", "text")); + + // When + callback.onEvent(event); + + // Then + verify(metadataManager).refreshSchema("ks", false, false); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/control/ControlConnectionTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/control/ControlConnectionTest.java new file mode 100644 index 00000000000..845c0435aa4 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/control/ControlConnectionTest.java @@ -0,0 +1,591 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.control; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.internal.core.channel.ChannelEvent; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.MockChannelFactoryHelper; +import com.datastax.oss.driver.internal.core.metadata.DistanceEvent; +import com.datastax.oss.driver.internal.core.metadata.NodeStateEvent; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(DataProviderRunner.class) +public class ControlConnectionTest extends ControlConnectionTestBase { + + @Test + public void should_close_successfully_if_it_was_never_init() { + // When + CompletionStage closeFuture = controlConnection.forceCloseAsync(); + + // Then + assertThatStage(closeFuture).isSuccess(); + } + + @Test + public void should_init_with_first_contact_point_if_reachable() { + // Given + DriverChannel channel1 = newMockDriverChannel(1); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory).success(node1, channel1).build(); + + // When + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + waitForPendingAdminTasks(); + + // Then + assertThatStage(initFuture).isSuccess(); + assertThat(controlConnection.channel()).isEqualTo(channel1); + verify(eventBus).fire(ChannelEvent.channelOpened(node1)); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_always_return_same_init_future() { + // Given + DriverChannel channel1 = newMockDriverChannel(1); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory).success(node1, channel1).build(); + + // When + CompletionStage initFuture1 = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + CompletionStage initFuture2 = controlConnection.init(false, false, false); + + // Then + assertThatStage(initFuture1).isEqualTo(initFuture2); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_init_with_second_contact_point_if_first_one_fails() { + // Given + DriverChannel channel2 = newMockDriverChannel(2); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .failure(node1, "mock failure") + .success(node2, channel2) + .build(); + + // When + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + factoryHelper.waitForCall(node2); + waitForPendingAdminTasks(); + + // Then + assertThatStage(initFuture) + .isSuccess(v -> assertThat(controlConnection.channel()).isEqualTo(channel2)); + verify(eventBus).fire(ChannelEvent.controlConnectionFailed(node1)); + verify(eventBus).fire(ChannelEvent.channelOpened(node2)); + // each attempt tries all nodes, so there is no reconnection + verify(reconnectionPolicy, never()).newNodeSchedule(any(Node.class)); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_fail_to_init_if_all_contact_points_fail() { + // Given + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .failure(node1, "mock failure") + .failure(node2, "mock failure") + .build(); + + // When + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + factoryHelper.waitForCall(node2); + waitForPendingAdminTasks(); + + // Then + assertThatStage(initFuture).isFailed(); + verify(eventBus).fire(ChannelEvent.controlConnectionFailed(node1)); + verify(eventBus).fire(ChannelEvent.controlConnectionFailed(node2)); + // no reconnections at init + verify(reconnectionPolicy, never()).newNodeSchedule(any(Node.class)); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_reconnect_if_channel_goes_down() throws Exception { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node1, channel1) + .failure(node1, "mock failure") + .success(node2, channel2) + .build(); + + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + assertThat(controlConnection.channel()).isEqualTo(channel1); + verify(eventBus).fire(ChannelEvent.channelOpened(node1)); + + // When + channel1.close(); + waitForPendingAdminTasks(); + + // Then + // a reconnection was started + verify(reconnectionSchedule).nextDelay(); + factoryHelper.waitForCall(node1); + factoryHelper.waitForCall(node2); + waitForPendingAdminTasks(); + assertThat(controlConnection.channel()).isEqualTo(channel2); + verify(eventBus).fire(ChannelEvent.channelClosed(node1)); + verify(eventBus).fire(ChannelEvent.channelOpened(node2)); + verify(metadataManager).refreshNodes(); + verify(loadBalancingPolicyWrapper).init(); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_reconnect_if_node_becomes_ignored() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node1, channel1) + .success(node2, channel2) + .build(); + + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + assertThat(controlConnection.channel()).isEqualTo(channel1); + verify(eventBus).fire(ChannelEvent.channelOpened(node1)); + + // When + mockQueryPlan(node2); + eventBus.fire(new DistanceEvent(NodeDistance.IGNORED, node1)); + waitForPendingAdminTasks(); + + // Then + // an immediate reconnection was started + verify(reconnectionSchedule, never()).nextDelay(); + factoryHelper.waitForCall(node2); + waitForPendingAdminTasks(); + assertThat(controlConnection.channel()).isEqualTo(channel2); + verify(eventBus).fire(ChannelEvent.channelClosed(node1)); + verify(eventBus).fire(ChannelEvent.channelOpened(node2)); + verify(metadataManager).refreshNodes(); + verify(loadBalancingPolicyWrapper).init(); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_reconnect_if_node_is_removed() { + should_reconnect_if_event(NodeStateEvent.removed(node1)); + } + + @Test + public void should_reconnect_if_node_is_forced_down() { + should_reconnect_if_event(NodeStateEvent.changed(NodeState.UP, NodeState.FORCED_DOWN, node1)); + } + + private void should_reconnect_if_event(NodeStateEvent event) { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node1, channel1) + .success(node2, channel2) + .build(); + + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + assertThat(controlConnection.channel()).isEqualTo(channel1); + verify(eventBus).fire(ChannelEvent.channelOpened(node1)); + + // When + mockQueryPlan(node2); + eventBus.fire(event); + waitForPendingAdminTasks(); + + // Then + // an immediate reconnection was started + verify(reconnectionSchedule, never()).nextDelay(); + factoryHelper.waitForCall(node2); + waitForPendingAdminTasks(); + assertThat(controlConnection.channel()).isEqualTo(channel2); + verify(eventBus).fire(ChannelEvent.channelClosed(node1)); + verify(eventBus).fire(ChannelEvent.channelOpened(node2)); + verify(metadataManager).refreshNodes(); + verify(loadBalancingPolicyWrapper).init(); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_reconnect_if_node_became_ignored_during_reconnection_attempt() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + CompletableFuture channel2Future = new CompletableFuture<>(); + DriverChannel channel3 = newMockDriverChannel(3); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node1, channel1) + // reconnection + .pending(node2, channel2Future) + .success(node1, channel3) + .build(); + + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + assertThat(controlConnection.channel()).isEqualTo(channel1); + verify(eventBus).fire(ChannelEvent.channelOpened(node1)); + + mockQueryPlan(node2, node1); + // channel1 goes down, triggering a reconnection + channel1.close(); + waitForPendingAdminTasks(); + verify(eventBus).fire(ChannelEvent.channelClosed(node1)); + verify(reconnectionSchedule).nextDelay(); + // the reconnection to node2 is in progress + factoryHelper.waitForCall(node2); + + // When + // node2 becomes ignored + eventBus.fire(new DistanceEvent(NodeDistance.IGNORED, node2)); + // the reconnection to node2 completes + channel2Future.complete(channel2); + waitForPendingAdminTasks(); + + // Then + // The channel should get closed and we should try the next node + verify(channel2).forceClose(); + factoryHelper.waitForCall(node1); + } + + @Test + public void should_reconnect_if_node_was_removed_during_reconnection_attempt() { + should_reconnect_if_event_during_reconnection_attempt(NodeStateEvent.removed(node2)); + } + + @Test + public void should_reconnect_if_node_was_forced_down_during_reconnection_attempt() { + should_reconnect_if_event_during_reconnection_attempt( + NodeStateEvent.changed(NodeState.UP, NodeState.FORCED_DOWN, node2)); + } + + private void should_reconnect_if_event_during_reconnection_attempt(NodeStateEvent event) { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + CompletableFuture channel2Future = new CompletableFuture<>(); + DriverChannel channel3 = newMockDriverChannel(3); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node1, channel1) + // reconnection + .pending(node2, channel2Future) + .success(node1, channel3) + .build(); + + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + assertThat(controlConnection.channel()).isEqualTo(channel1); + verify(eventBus).fire(ChannelEvent.channelOpened(node1)); + + mockQueryPlan(node2, node1); + // channel1 goes down, triggering a reconnection + channel1.close(); + waitForPendingAdminTasks(); + verify(eventBus).fire(ChannelEvent.channelClosed(node1)); + verify(reconnectionSchedule).nextDelay(); + // the reconnection to node2 is in progress + factoryHelper.waitForCall(node2); + + // When + // node2 goes into the new state + eventBus.fire(event); + // the reconnection to node2 completes + channel2Future.complete(channel2); + waitForPendingAdminTasks(); + + // Then + // The channel should get closed and we should try the next node + verify(channel2).forceClose(); + factoryHelper.waitForCall(node1); + } + + @Test + public void should_force_reconnection_if_pending() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofDays(1)); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node1, channel1) + .failure(node1, "mock failure") + .success(node2, channel2) + .build(); + + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + assertThat(controlConnection.channel()).isEqualTo(channel1); + verify(eventBus).fire(ChannelEvent.channelOpened(node1)); + + // the channel fails and a reconnection is scheduled for later + channel1.close(); + waitForPendingAdminTasks(); + verify(eventBus).fire(ChannelEvent.channelClosed(node1)); + verify(reconnectionSchedule).nextDelay(); + + // When + controlConnection.reconnectNow(); + factoryHelper.waitForCall(node1); + factoryHelper.waitForCall(node2); + waitForPendingAdminTasks(); + + // Then + assertThat(controlConnection.channel()).isEqualTo(channel2); + verify(eventBus).fire(ChannelEvent.channelOpened(node2)); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_force_reconnection_even_if_connected() { + // Given + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node1, channel1) + .failure(node1, "mock failure") + .success(node2, channel2) + .build(); + + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + assertThat(controlConnection.channel()).isEqualTo(channel1); + verify(eventBus).fire(ChannelEvent.channelOpened(node1)); + + // When + controlConnection.reconnectNow(); + + // Then + factoryHelper.waitForCall(node1); + factoryHelper.waitForCall(node2); + waitForPendingAdminTasks(); + assertThat(controlConnection.channel()).isEqualTo(channel2); + verify(channel1).forceClose(); + verify(eventBus).fire(ChannelEvent.channelClosed(node1)); + verify(eventBus).fire(ChannelEvent.channelOpened(node2)); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_not_force_reconnection_if_not_init() { + // When + controlConnection.reconnectNow(); + waitForPendingAdminTasks(); + + // Then + verify(reconnectionSchedule, never()).nextDelay(); + } + + @Test + public void should_not_force_reconnection_if_closed() { + // Given + DriverChannel channel1 = newMockDriverChannel(1); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory).success(node1, channel1).build(); + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + CompletionStage closeFuture = controlConnection.forceCloseAsync(); + assertThatStage(closeFuture).isSuccess(); + + // When + controlConnection.reconnectNow(); + waitForPendingAdminTasks(); + + // Then + verify(reconnectionSchedule, never()).nextDelay(); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_close_channel_when_closing() { + // Given + DriverChannel channel1 = newMockDriverChannel(1); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory).success(node1, channel1).build(); + + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + + // When + CompletionStage closeFuture = controlConnection.forceCloseAsync(); + waitForPendingAdminTasks(); + + // Then + assertThatStage(closeFuture).isSuccess(); + verify(channel1).forceClose(); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_close_channel_if_closed_during_reconnection() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + CompletableFuture channel2Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node1, channel1) + .failure(node1, "mock failure") + .pending(node2, channel2Future) + .build(); + + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + assertThat(controlConnection.channel()).isEqualTo(channel1); + verify(eventBus).fire(ChannelEvent.channelOpened(node1)); + + // the channel fails and a reconnection is scheduled + channel1.close(); + waitForPendingAdminTasks(); + verify(eventBus).fire(ChannelEvent.channelClosed(node1)); + verify(reconnectionSchedule).nextDelay(); + factoryHelper.waitForCall(node1); + // channel2 starts initializing (but the future is not completed yet) + factoryHelper.waitForCall(node2); + + // When + // the control connection gets closed before channel2 initialization is complete + controlConnection.forceCloseAsync(); + waitForPendingAdminTasks(); + channel2Future.complete(channel2); + waitForPendingAdminTasks(); + + // Then + verify(channel2).forceClose(); + // no event because the control connection never "owned" the channel + verify(eventBus, never()).fire(ChannelEvent.channelOpened(node2)); + verify(eventBus, never()).fire(ChannelEvent.channelClosed(node2)); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_handle_channel_failure_if_closed_during_reconnection() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + CompletableFuture channel1Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node1, channel1) + .pending(node1, channel1Future) + .success(node2, channel2) + .build(); + + CompletionStage initFuture = controlConnection.init(false, false, false); + factoryHelper.waitForCall(node1); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + assertThat(controlConnection.channel()).isEqualTo(channel1); + verify(eventBus).fire(ChannelEvent.channelOpened(node1)); + + // the channel fails and a reconnection is scheduled + channel1.close(); + waitForPendingAdminTasks(); + verify(eventBus).fire(ChannelEvent.channelClosed(node1)); + verify(reconnectionSchedule).nextDelay(); + // channel1 starts initializing (but the future is not completed yet) + factoryHelper.waitForCall(node1); + + // When + // the control connection gets closed before channel1 initialization fails + controlConnection.forceCloseAsync(); + channel1Future.completeExceptionally(new Exception("mock failure")); + waitForPendingAdminTasks(); + + // Then + // should never try channel2 because the reconnection has detected that it can stop after the + // first failure + factoryHelper.verifyNoMoreCalls(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/control/ControlConnectionTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/control/ControlConnectionTestBase.java new file mode 100644 index 00000000000..a25b7c97f52 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/control/ControlConnectionTestBase.java @@ -0,0 +1,186 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.control; + +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.channel.ChannelFactory; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.DriverChannelOptions; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.metadata.LoadBalancingPolicyWrapper; +import com.datastax.oss.driver.internal.core.metadata.MetadataManager; +import com.datastax.oss.driver.internal.core.metadata.TestNodeFactory; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.Uninterruptibles; +import io.netty.channel.Channel; +import io.netty.channel.DefaultChannelPromise; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoop; +import io.netty.util.concurrent.Future; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Exchanger; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.After; +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +abstract class ControlConnectionTestBase { + protected static final InetSocketAddress ADDRESS1 = new InetSocketAddress("127.0.0.1", 9042); + protected static final InetSocketAddress ADDRESS2 = new InetSocketAddress("127.0.0.2", 9042); + + @Mock protected InternalDriverContext context; + @Mock protected DriverConfig config; + @Mock protected DriverExecutionProfile defaultProfile; + @Mock protected ReconnectionPolicy reconnectionPolicy; + @Mock protected ReconnectionPolicy.ReconnectionSchedule reconnectionSchedule; + @Mock protected NettyOptions nettyOptions; + protected DefaultEventLoopGroup adminEventLoopGroup; + protected EventBus eventBus; + @Mock protected ChannelFactory channelFactory; + protected Exchanger> channelFactoryFuture; + @Mock protected LoadBalancingPolicyWrapper loadBalancingPolicyWrapper; + @Mock protected MetadataManager metadataManager; + @Mock protected MetricsFactory metricsFactory; + + protected DefaultNode node1; + protected DefaultNode node2; + + protected ControlConnection controlConnection; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + adminEventLoopGroup = new DefaultEventLoopGroup(1); + + when(context.getNettyOptions()).thenReturn(nettyOptions); + when(nettyOptions.adminEventExecutorGroup()).thenReturn(adminEventLoopGroup); + eventBus = spy(new EventBus("test")); + when(context.getEventBus()).thenReturn(eventBus); + when(context.getChannelFactory()).thenReturn(channelFactory); + + channelFactoryFuture = new Exchanger<>(); + when(channelFactory.connect(any(Node.class), any(DriverChannelOptions.class))) + .thenAnswer( + invocation -> { + CompletableFuture channelFuture = new CompletableFuture<>(); + channelFactoryFuture.exchange(channelFuture, 100, TimeUnit.MILLISECONDS); + return channelFuture; + }); + + when(context.getConfig()).thenReturn(config); + when(config.getDefaultProfile()).thenReturn(defaultProfile); + when(defaultProfile.getBoolean(DefaultDriverOption.RECONNECT_ON_INIT)).thenReturn(false); + + when(context.getReconnectionPolicy()).thenReturn(reconnectionPolicy); + // Child classes only cover "runtime" reconnections when the driver is already initialized + when(reconnectionPolicy.newControlConnectionSchedule(false)).thenReturn(reconnectionSchedule); + // By default, set a large reconnection delay. Tests that care about reconnection will override + // it. + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofDays(1)); + + when(context.getLoadBalancingPolicyWrapper()).thenReturn(loadBalancingPolicyWrapper); + + when(context.getMetricsFactory()).thenReturn(metricsFactory); + node1 = TestNodeFactory.newNode(1, context); + node2 = TestNodeFactory.newNode(2, context); + mockQueryPlan(node1, node2); + + when(metadataManager.refreshNodes()).thenReturn(CompletableFuture.completedFuture(null)); + when(context.getMetadataManager()).thenReturn(metadataManager); + + when(context.getConfig()).thenReturn(config); + when(config.getDefaultProfile()).thenReturn(defaultProfile); + when(defaultProfile.getBoolean(DefaultDriverOption.CONNECTION_WARN_INIT_ERROR)) + .thenReturn(false); + + controlConnection = new ControlConnection(context); + } + + protected void mockQueryPlan(Node... nodes) { + when(loadBalancingPolicyWrapper.newQueryPlan()) + .thenAnswer( + i -> { + ConcurrentLinkedQueue queryPlan = new ConcurrentLinkedQueue<>(); + for (Node node : nodes) { + queryPlan.offer(node); + } + return queryPlan; + }); + } + + @After + public void teardown() { + adminEventLoopGroup.shutdownGracefully(100, 200, TimeUnit.MILLISECONDS); + } + + protected DriverChannel newMockDriverChannel(int id) { + DriverChannel driverChannel = mock(DriverChannel.class); + Channel channel = mock(Channel.class); + EventLoop adminExecutor = adminEventLoopGroup.next(); + DefaultChannelPromise closeFuture = new DefaultChannelPromise(channel, adminExecutor); + when(driverChannel.close()) + .thenAnswer( + i -> { + closeFuture.trySuccess(null); + return closeFuture; + }); + when(driverChannel.forceClose()) + .thenAnswer( + i -> { + closeFuture.trySuccess(null); + return closeFuture; + }); + when(driverChannel.closeFuture()).thenReturn(closeFuture); + when(driverChannel.toString()).thenReturn("channel" + id); + when(driverChannel.getEndPoint()) + .thenReturn(new DefaultEndPoint(new InetSocketAddress("127.0.0." + id, 9042))); + return driverChannel; + } + + // Wait for all the tasks on the admin executor to complete. + protected void waitForPendingAdminTasks() { + // This works because the event loop group is single-threaded + Future f = adminEventLoopGroup.schedule(() -> null, 5, TimeUnit.NANOSECONDS); + try { + Uninterruptibles.getUninterruptibly(f, 100, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + fail("unexpected error", e.getCause()); + } catch (TimeoutException e) { + fail("timed out while waiting for admin tasks to complete", e); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareHandlerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareHandlerTest.java new file mode 100644 index 00000000000..ced7d095ee1 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareHandlerTest.java @@ -0,0 +1,373 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.retry.RetryDecision; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.servererrors.OverloadedException; +import com.datastax.oss.driver.internal.core.channel.ResponseCallback; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.request.Prepare; +import com.datastax.oss.protocol.internal.response.Error; +import com.datastax.oss.protocol.internal.response.result.ColumnSpec; +import com.datastax.oss.protocol.internal.response.result.Prepared; +import com.datastax.oss.protocol.internal.response.result.RawType; +import com.datastax.oss.protocol.internal.response.result.RowsMetadata; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class CqlPrepareHandlerTest { + + private static final DefaultPrepareRequest PREPARE_REQUEST = + new DefaultPrepareRequest("mock query"); + + @Mock private Node node1; + @Mock private Node node2; + @Mock private Node node3; + + private final Map payload = + ImmutableMap.of("key1", ByteBuffer.wrap(new byte[] {1, 2, 3, 4})); + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void should_prepare_on_first_node_and_reprepare_on_others() { + RequestHandlerTestHarness.Builder harnessBuilder = RequestHandlerTestHarness.builder(); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + PoolBehavior node3Behavior = harnessBuilder.customBehavior(node3); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + + CompletionStage prepareFuture = + new CqlPrepareHandler(PREPARE_REQUEST, harness.getSession(), harness.getContext(), "test") + .handle(); + + node1Behavior.verifyWrite(); + node1Behavior.setWriteSuccess(); + node1Behavior.setResponseSuccess(defaultFrameOf(simplePrepared())); + + // The future waits for the reprepare attempt on other nodes, so it's not done yet. + assertThatStage(prepareFuture).isNotDone(); + + // Should now reprepare on the remaining nodes: + node2Behavior.verifyWrite(); + node2Behavior.setWriteSuccess(); + node2Behavior.setResponseSuccess(defaultFrameOf(simplePrepared())); + + node3Behavior.verifyWrite(); + node3Behavior.setWriteSuccess(); + node3Behavior.setResponseSuccess(defaultFrameOf(simplePrepared())); + + assertThatStage(prepareFuture).isSuccess(CqlPrepareHandlerTest::assertMatchesSimplePrepared); + } + } + + @Test + public void should_not_reprepare_on_other_nodes_if_disabled_in_config() { + RequestHandlerTestHarness.Builder harnessBuilder = RequestHandlerTestHarness.builder(); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + PoolBehavior node3Behavior = harnessBuilder.customBehavior(node3); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + + DriverExecutionProfile config = harness.getContext().getConfig().getDefaultProfile(); + when(config.getBoolean(DefaultDriverOption.PREPARE_ON_ALL_NODES)).thenReturn(false); + + CompletionStage prepareFuture = + new CqlPrepareHandler(PREPARE_REQUEST, harness.getSession(), harness.getContext(), "test") + .handle(); + + node1Behavior.verifyWrite(); + node1Behavior.setWriteSuccess(); + node1Behavior.setResponseSuccess(defaultFrameOf(simplePrepared())); + + // The future should complete immediately: + assertThatStage(prepareFuture).isSuccess(); + + // And the other nodes should not be contacted: + node2Behavior.verifyNoWrite(); + node3Behavior.verifyNoWrite(); + } + } + + @Test + public void should_ignore_errors_while_repreparing_on_other_nodes() { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withResponse(node1, defaultFrameOf(simplePrepared())); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + PoolBehavior node3Behavior = harnessBuilder.customBehavior(node3); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + + CompletionStage prepareFuture = + new CqlPrepareHandler(PREPARE_REQUEST, harness.getSession(), harness.getContext(), "test") + .handle(); + + assertThatStage(prepareFuture).isNotDone(); + + // Other nodes fail, the future should still succeed when all done + node2Behavior.verifyWrite(); + node2Behavior.setWriteSuccess(); + node2Behavior.setResponseSuccess( + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.SERVER_ERROR, "mock error"))); + + node3Behavior.verifyWrite(); + node3Behavior.setWriteFailure(new RuntimeException("mock error")); + + assertThatStage(prepareFuture).isSuccess(CqlPrepareHandlerTest::assertMatchesSimplePrepared); + } + } + + @Test + public void should_retry_initial_prepare_if_recoverable_error() { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder() + .withResponse( + node1, + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.OVERLOADED, "mock message"))) + .withResponse(node2, defaultFrameOf(simplePrepared())); + PoolBehavior node3Behavior = harnessBuilder.customBehavior(node3); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + + // Make node1's error recoverable, will switch to node2 + when(harness + .getContext() + .getRetryPolicy(anyString()) + .onErrorResponse(eq(PREPARE_REQUEST), any(OverloadedException.class), eq(0))) + .thenReturn(RetryDecision.RETRY_NEXT); + + CompletionStage prepareFuture = + new CqlPrepareHandler(PREPARE_REQUEST, harness.getSession(), harness.getContext(), "test") + .handle(); + + // Success on node2, reprepare on node3 + assertThatStage(prepareFuture).isNotDone(); + node3Behavior.verifyWrite(); + node3Behavior.setWriteSuccess(); + node3Behavior.setResponseSuccess(defaultFrameOf(simplePrepared())); + + assertThatStage(prepareFuture).isSuccess(CqlPrepareHandlerTest::assertMatchesSimplePrepared); + } + } + + @Test + public void should_not_retry_initial_prepare_if_unrecoverable_error() { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder() + .withResponse( + node1, + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.OVERLOADED, "mock message"))); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + PoolBehavior node3Behavior = harnessBuilder.customBehavior(node3); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + + // Make node1's error unrecoverable, will rethrow + when(harness + .getContext() + .getRetryPolicy(anyString()) + .onErrorResponse(eq(PREPARE_REQUEST), any(OverloadedException.class), eq(0))) + .thenReturn(RetryDecision.RETHROW); + + CompletionStage prepareFuture = + new CqlPrepareHandler(PREPARE_REQUEST, harness.getSession(), harness.getContext(), "test") + .handle(); + + // Success on node2, reprepare on node3 + assertThatStage(prepareFuture) + .isFailed( + error -> { + assertThat(error).isInstanceOf(OverloadedException.class); + node2Behavior.verifyNoWrite(); + node3Behavior.verifyNoWrite(); + }); + } + } + + @Test + public void should_fail_if_retry_policy_ignores_error() { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder() + .withResponse( + node1, + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.OVERLOADED, "mock message"))); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + PoolBehavior node3Behavior = harnessBuilder.customBehavior(node3); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + + // Make node1's error unrecoverable, will rethrow + RetryPolicy mockRetryPolicy = + harness.getContext().getRetryPolicy(DriverExecutionProfile.DEFAULT_NAME); + when(mockRetryPolicy.onErrorResponse( + eq(PREPARE_REQUEST), any(OverloadedException.class), eq(0))) + .thenReturn(RetryDecision.IGNORE); + + CompletionStage prepareFuture = + new CqlPrepareHandler(PREPARE_REQUEST, harness.getSession(), harness.getContext(), "test") + .handle(); + + // Success on node2, reprepare on node3 + assertThatStage(prepareFuture) + .isFailed( + error -> { + assertThat(error) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "IGNORE decisions are not allowed for prepare requests, " + + "please fix your retry policy."); + node2Behavior.verifyNoWrite(); + node3Behavior.verifyNoWrite(); + }); + } + } + + @Test + public void should_propagate_custom_payload_on_single_node() { + RequestHandlerTestHarness.Builder harnessBuilder = RequestHandlerTestHarness.builder(); + DefaultPrepareRequest prepareRequest = + new DefaultPrepareRequest( + SimpleStatement.newInstance("irrelevant").setCustomPayload(payload)); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + PoolBehavior node3Behavior = harnessBuilder.customBehavior(node3); + node1Behavior.setResponseSuccess(defaultFrameOf(simplePrepared())); + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + DriverExecutionProfile config = harness.getContext().getConfig().getDefaultProfile(); + when(config.getBoolean(DefaultDriverOption.PREPARE_ON_ALL_NODES)).thenReturn(false); + CompletionStage prepareFuture = + new CqlPrepareHandler(prepareRequest, harness.getSession(), harness.getContext(), "test") + .handle(); + verify(node1Behavior.channel) + .write(any(Prepare.class), anyBoolean(), eq(payload), any(ResponseCallback.class)); + node2Behavior.verifyNoWrite(); + node3Behavior.verifyNoWrite(); + assertThatStage(prepareFuture).isSuccess(CqlPrepareHandlerTest::assertMatchesSimplePrepared); + } + } + + @Test + public void should_propagate_custom_payload_on_all_nodes() { + RequestHandlerTestHarness.Builder harnessBuilder = RequestHandlerTestHarness.builder(); + DefaultPrepareRequest prepareRequest = + new DefaultPrepareRequest( + SimpleStatement.newInstance("irrelevant").setCustomPayload(payload)); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + PoolBehavior node3Behavior = harnessBuilder.customBehavior(node3); + node1Behavior.setResponseSuccess(defaultFrameOf(simplePrepared())); + node2Behavior.setResponseSuccess(defaultFrameOf(simplePrepared())); + node3Behavior.setResponseSuccess(defaultFrameOf(simplePrepared())); + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + DriverExecutionProfile config = harness.getContext().getConfig().getDefaultProfile(); + when(config.getBoolean(DefaultDriverOption.PREPARE_ON_ALL_NODES)).thenReturn(true); + CompletionStage prepareFuture = + new CqlPrepareHandler(prepareRequest, harness.getSession(), harness.getContext(), "test") + .handle(); + verify(node1Behavior.channel) + .write(any(Prepare.class), anyBoolean(), eq(payload), any(ResponseCallback.class)); + verify(node2Behavior.channel) + .write(any(Prepare.class), anyBoolean(), eq(payload), any(ResponseCallback.class)); + verify(node3Behavior.channel) + .write(any(Prepare.class), anyBoolean(), eq(payload), any(ResponseCallback.class)); + assertThatStage(prepareFuture).isSuccess(CqlPrepareHandlerTest::assertMatchesSimplePrepared); + } + } + + private static Frame defaultFrameOf(Message responseMessage) { + return Frame.forResponse( + DefaultProtocolVersion.V4.getCode(), + 0, + null, + Frame.NO_PAYLOAD, + Collections.emptyList(), + responseMessage); + } + + private static Message simplePrepared() { + RowsMetadata variablesMetadata = + new RowsMetadata( + ImmutableList.of( + new ColumnSpec( + "ks", + "table", + "key", + 0, + RawType.PRIMITIVES.get(ProtocolConstants.DataType.VARCHAR))), + null, + new int[] {0}, + null); + RowsMetadata resultMetadata = + new RowsMetadata( + ImmutableList.of( + new ColumnSpec( + "ks", + "table", + "message", + 0, + RawType.PRIMITIVES.get(ProtocolConstants.DataType.VARCHAR))), + null, + new int[] {}, + null); + return new Prepared( + Bytes.fromHexString("0xffff").array(), null, variablesMetadata, resultMetadata); + } + + private static void assertMatchesSimplePrepared(PreparedStatement statement) { + assertThat(Bytes.toHexString(statement.getId())).isEqualTo("0xffff"); + + ColumnDefinitions variableDefinitions = statement.getVariableDefinitions(); + assertThat(variableDefinitions).hasSize(1); + assertThat(variableDefinitions.get(0).getName().asInternal()).isEqualTo("key"); + + ColumnDefinitions resultSetDefinitions = statement.getResultSetDefinitions(); + assertThat(resultSetDefinitions).hasSize(1); + assertThat(resultSetDefinitions.get(0).getName().asInternal()).isEqualTo("message"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerRetryTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerRetryTest.java new file mode 100644 index 00000000000..0e503a134c8 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerRetryTest.java @@ -0,0 +1,542 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.TestDataProviders; +import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.HeartbeatException; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import com.datastax.oss.driver.api.core.retry.RetryDecision; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.servererrors.BootstrappingException; +import com.datastax.oss.driver.api.core.servererrors.DefaultWriteType; +import com.datastax.oss.driver.api.core.servererrors.InvalidQueryException; +import com.datastax.oss.driver.api.core.servererrors.ReadTimeoutException; +import com.datastax.oss.driver.api.core.servererrors.ServerError; +import com.datastax.oss.driver.api.core.servererrors.UnavailableException; +import com.datastax.oss.driver.api.core.servererrors.WriteTimeoutException; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.Error; +import com.datastax.oss.protocol.internal.response.error.ReadTimeout; +import com.datastax.oss.protocol.internal.response.error.Unavailable; +import com.datastax.oss.protocol.internal.response.error.WriteTimeout; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Iterator; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class CqlRequestHandlerRetryTest extends CqlRequestHandlerTestBase { + + @Test + @UseDataProvider("allIdempotenceConfigs") + public void should_always_try_next_node_if_bootstrapping( + boolean defaultIdempotence, SimpleStatement statement) { + try (RequestHandlerTestHarness harness = + RequestHandlerTestHarness.builder() + .withDefaultIdempotence(defaultIdempotence) + .withResponse( + node1, + defaultFrameOf( + new Error(ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING, "mock message"))) + .withResponse(node2, defaultFrameOf(singleRow())) + .build()) { + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + + assertThatStage(resultSetFuture) + .isSuccess( + resultSet -> { + Iterator rows = resultSet.currentPage().iterator(); + assertThat(rows.hasNext()).isTrue(); + assertThat(rows.next().getString("message")).isEqualTo("hello, world"); + + ExecutionInfo executionInfo = resultSet.getExecutionInfo(); + assertThat(executionInfo.getCoordinator()).isEqualTo(node2); + assertThat(executionInfo.getErrors()).hasSize(1); + assertThat(executionInfo.getErrors().get(0).getKey()).isEqualTo(node1); + assertThat(executionInfo.getErrors().get(0).getValue()) + .isInstanceOf(BootstrappingException.class); + assertThat(executionInfo.getIncomingPayload()).isEmpty(); + assertThat(executionInfo.getPagingState()).isNull(); + assertThat(executionInfo.getSpeculativeExecutionCount()).isEqualTo(0); + assertThat(executionInfo.getSuccessfulExecutionIndex()).isEqualTo(0); + assertThat(executionInfo.getWarnings()).isEmpty(); + + verifyNoMoreInteractions(harness.getContext().getRetryPolicy(anyString())); + }); + } + } + + @Test + @UseDataProvider("allIdempotenceConfigs") + public void should_always_rethrow_query_validation_error( + boolean defaultIdempotence, SimpleStatement statement) { + try (RequestHandlerTestHarness harness = + RequestHandlerTestHarness.builder() + .withDefaultIdempotence(defaultIdempotence) + .withResponse( + node1, + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.INVALID, "mock message"))) + .build()) { + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + + assertThatStage(resultSetFuture) + .isFailed( + error -> { + assertThat(error) + .isInstanceOf(InvalidQueryException.class) + .hasMessage("mock message"); + verifyNoMoreInteractions(harness.getContext().getRetryPolicy(anyString())); + + verify(nodeMetricUpdater1) + .incrementCounter( + DefaultNodeMetric.OTHER_ERRORS, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(1)) + .isEnabled(DefaultNodeMetric.CQL_MESSAGES, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1) + .updateTimer( + eq(DefaultNodeMetric.CQL_MESSAGES), + eq(DriverExecutionProfile.DEFAULT_NAME), + anyLong(), + eq(TimeUnit.NANOSECONDS)); + verifyNoMoreInteractions(nodeMetricUpdater1); + }); + } + } + + @Test + @UseDataProvider("failureAndIdempotent") + public void should_try_next_node_if_idempotent_and_retry_policy_decides_so( + FailureScenario failureScenario, boolean defaultIdempotence, SimpleStatement statement) { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + failureScenario.mockRequestError(harnessBuilder, node1); + harnessBuilder.withResponse(node2, defaultFrameOf(singleRow())); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + failureScenario.mockRetryPolicyDecision( + harness.getContext().getRetryPolicy(anyString()), RetryDecision.RETRY_NEXT); + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + + assertThatStage(resultSetFuture) + .isSuccess( + resultSet -> { + Iterator rows = resultSet.currentPage().iterator(); + assertThat(rows.hasNext()).isTrue(); + assertThat(rows.next().getString("message")).isEqualTo("hello, world"); + + ExecutionInfo executionInfo = resultSet.getExecutionInfo(); + assertThat(executionInfo.getCoordinator()).isEqualTo(node2); + assertThat(executionInfo.getErrors()).hasSize(1); + assertThat(executionInfo.getErrors().get(0).getKey()).isEqualTo(node1); + + verify(nodeMetricUpdater1) + .incrementCounter( + failureScenario.errorMetric, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1) + .incrementCounter( + DefaultNodeMetric.RETRIES, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1) + .incrementCounter( + failureScenario.retryMetric, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(1)) + .isEnabled(DefaultNodeMetric.CQL_MESSAGES, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(1)) + .updateTimer( + eq(DefaultNodeMetric.CQL_MESSAGES), + eq(DriverExecutionProfile.DEFAULT_NAME), + anyLong(), + eq(TimeUnit.NANOSECONDS)); + verifyNoMoreInteractions(nodeMetricUpdater1); + }); + } + } + + @Test + @UseDataProvider("failureAndIdempotent") + public void should_try_same_node_if_idempotent_and_retry_policy_decides_so( + FailureScenario failureScenario, boolean defaultIdempotence, SimpleStatement statement) { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + failureScenario.mockRequestError(harnessBuilder, node1); + harnessBuilder.withResponse(node1, defaultFrameOf(singleRow())); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + failureScenario.mockRetryPolicyDecision( + harness.getContext().getRetryPolicy(anyString()), RetryDecision.RETRY_SAME); + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + + assertThatStage(resultSetFuture) + .isSuccess( + resultSet -> { + Iterator rows = resultSet.currentPage().iterator(); + assertThat(rows.hasNext()).isTrue(); + assertThat(rows.next().getString("message")).isEqualTo("hello, world"); + + ExecutionInfo executionInfo = resultSet.getExecutionInfo(); + assertThat(executionInfo.getCoordinator()).isEqualTo(node1); + assertThat(executionInfo.getErrors()).hasSize(1); + assertThat(executionInfo.getErrors().get(0).getKey()).isEqualTo(node1); + + verify(nodeMetricUpdater1) + .incrementCounter( + failureScenario.errorMetric, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1) + .incrementCounter( + DefaultNodeMetric.RETRIES, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1) + .incrementCounter( + failureScenario.retryMetric, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(2)) + .isEnabled(DefaultNodeMetric.CQL_MESSAGES, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(2)) + .updateTimer( + eq(DefaultNodeMetric.CQL_MESSAGES), + eq(DriverExecutionProfile.DEFAULT_NAME), + anyLong(), + eq(TimeUnit.NANOSECONDS)); + verifyNoMoreInteractions(nodeMetricUpdater1); + }); + } + } + + @Test + @UseDataProvider("failureAndIdempotent") + public void should_ignore_error_if_idempotent_and_retry_policy_decides_so( + FailureScenario failureScenario, boolean defaultIdempotence, SimpleStatement statement) { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + failureScenario.mockRequestError(harnessBuilder, node1); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + failureScenario.mockRetryPolicyDecision( + harness.getContext().getRetryPolicy(anyString()), RetryDecision.IGNORE); + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + + assertThatStage(resultSetFuture) + .isSuccess( + resultSet -> { + Iterator rows = resultSet.currentPage().iterator(); + assertThat(rows.hasNext()).isFalse(); + + ExecutionInfo executionInfo = resultSet.getExecutionInfo(); + assertThat(executionInfo.getCoordinator()).isEqualTo(node1); + assertThat(executionInfo.getErrors()).hasSize(0); + + verify(nodeMetricUpdater1) + .incrementCounter( + failureScenario.errorMetric, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1) + .incrementCounter( + DefaultNodeMetric.IGNORES, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1) + .incrementCounter( + failureScenario.ignoreMetric, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(1)) + .isEnabled(DefaultNodeMetric.CQL_MESSAGES, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(1)) + .updateTimer( + eq(DefaultNodeMetric.CQL_MESSAGES), + eq(DriverExecutionProfile.DEFAULT_NAME), + anyLong(), + eq(TimeUnit.NANOSECONDS)); + verifyNoMoreInteractions(nodeMetricUpdater1); + }); + } + } + + @Test + @UseDataProvider("failureAndIdempotent") + public void should_rethrow_error_if_idempotent_and_retry_policy_decides_so( + FailureScenario failureScenario, boolean defaultIdempotence, SimpleStatement statement) { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + failureScenario.mockRequestError(harnessBuilder, node1); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + + failureScenario.mockRetryPolicyDecision( + harness.getContext().getRetryPolicy(anyString()), RetryDecision.RETHROW); + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + + assertThatStage(resultSetFuture) + .isFailed( + error -> { + assertThat(error).isInstanceOf(failureScenario.expectedExceptionClass); + + verify(nodeMetricUpdater1) + .incrementCounter( + failureScenario.errorMetric, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(1)) + .isEnabled(DefaultNodeMetric.CQL_MESSAGES, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(1)) + .updateTimer( + eq(DefaultNodeMetric.CQL_MESSAGES), + eq(DriverExecutionProfile.DEFAULT_NAME), + anyLong(), + eq(TimeUnit.NANOSECONDS)); + verifyNoMoreInteractions(nodeMetricUpdater1); + }); + } + } + + @Test + @UseDataProvider("failureAndNotIdempotent") + public void should_rethrow_error_if_not_idempotent_and_error_unsafe_or_policy_rethrows( + FailureScenario failureScenario, boolean defaultIdempotence, SimpleStatement statement) { + + // For two of the possible exceptions, the retry policy is called even if the statement is not + // idempotent + boolean shouldCallRetryPolicy = + (failureScenario.expectedExceptionClass.equals(UnavailableException.class) + || failureScenario.expectedExceptionClass.equals(ReadTimeoutException.class)); + + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + failureScenario.mockRequestError(harnessBuilder, node1); + harnessBuilder.withResponse(node2, defaultFrameOf(singleRow())); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + + if (shouldCallRetryPolicy) { + failureScenario.mockRetryPolicyDecision( + harness.getContext().getRetryPolicy(anyString()), RetryDecision.RETHROW); + } + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + + assertThatStage(resultSetFuture) + .isFailed( + error -> { + assertThat(error).isInstanceOf(failureScenario.expectedExceptionClass); + // When non idempotent, the policy is bypassed completely: + if (!shouldCallRetryPolicy) { + verifyNoMoreInteractions(harness.getContext().getRetryPolicy(anyString())); + } + + verify(nodeMetricUpdater1) + .incrementCounter( + failureScenario.errorMetric, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(1)) + .isEnabled(DefaultNodeMetric.CQL_MESSAGES, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1, atMost(1)) + .updateTimer( + eq(DefaultNodeMetric.CQL_MESSAGES), + eq(DriverExecutionProfile.DEFAULT_NAME), + anyLong(), + eq(TimeUnit.NANOSECONDS)); + verifyNoMoreInteractions(nodeMetricUpdater1); + }); + } + } + + /** + * Sets up the mocks to simulate an error from a node, and make the retry policy return a given + * decision for that error. + */ + private abstract static class FailureScenario { + private final Class expectedExceptionClass; + final DefaultNodeMetric errorMetric; + final DefaultNodeMetric retryMetric; + final DefaultNodeMetric ignoreMetric; + + protected FailureScenario( + Class expectedExceptionClass, + DefaultNodeMetric errorMetric, + DefaultNodeMetric retryMetric, + DefaultNodeMetric ignoreMetric) { + this.expectedExceptionClass = expectedExceptionClass; + this.errorMetric = errorMetric; + this.retryMetric = retryMetric; + this.ignoreMetric = ignoreMetric; + } + + abstract void mockRequestError(RequestHandlerTestHarness.Builder builder, Node node); + + abstract void mockRetryPolicyDecision(RetryPolicy policy, RetryDecision decision); + } + + @DataProvider + public static Object[][] failure() { + return TestDataProviders.fromList( + new FailureScenario( + ReadTimeoutException.class, + DefaultNodeMetric.READ_TIMEOUTS, + DefaultNodeMetric.RETRIES_ON_READ_TIMEOUT, + DefaultNodeMetric.IGNORES_ON_READ_TIMEOUT) { + @Override + public void mockRequestError(RequestHandlerTestHarness.Builder builder, Node node) { + builder.withResponse( + node, + defaultFrameOf( + new ReadTimeout( + "mock message", ProtocolConstants.ConsistencyLevel.LOCAL_ONE, 1, 2, true))); + } + + @Override + public void mockRetryPolicyDecision(RetryPolicy policy, RetryDecision decision) { + when(policy.onReadTimeout( + any(SimpleStatement.class), + eq(DefaultConsistencyLevel.LOCAL_ONE), + eq(2), + eq(1), + eq(true), + eq(0))) + .thenReturn(decision); + } + }, + new FailureScenario( + WriteTimeoutException.class, + DefaultNodeMetric.WRITE_TIMEOUTS, + DefaultNodeMetric.RETRIES_ON_WRITE_TIMEOUT, + DefaultNodeMetric.IGNORES_ON_WRITE_TIMEOUT) { + @Override + public void mockRequestError(RequestHandlerTestHarness.Builder builder, Node node) { + builder.withResponse( + node, + defaultFrameOf( + new WriteTimeout( + "mock message", + ProtocolConstants.ConsistencyLevel.LOCAL_ONE, + 1, + 2, + ProtocolConstants.WriteType.SIMPLE))); + } + + @Override + public void mockRetryPolicyDecision(RetryPolicy policy, RetryDecision decision) { + when(policy.onWriteTimeout( + any(SimpleStatement.class), + eq(DefaultConsistencyLevel.LOCAL_ONE), + eq(DefaultWriteType.SIMPLE), + eq(2), + eq(1), + eq(0))) + .thenReturn(decision); + } + }, + new FailureScenario( + UnavailableException.class, + DefaultNodeMetric.UNAVAILABLES, + DefaultNodeMetric.RETRIES_ON_UNAVAILABLE, + DefaultNodeMetric.IGNORES_ON_UNAVAILABLE) { + @Override + public void mockRequestError(RequestHandlerTestHarness.Builder builder, Node node) { + builder.withResponse( + node, + defaultFrameOf( + new Unavailable( + "mock message", ProtocolConstants.ConsistencyLevel.LOCAL_ONE, 2, 1))); + } + + @Override + public void mockRetryPolicyDecision(RetryPolicy policy, RetryDecision decision) { + when(policy.onUnavailable( + any(SimpleStatement.class), + eq(DefaultConsistencyLevel.LOCAL_ONE), + eq(2), + eq(1), + eq(0))) + .thenReturn(decision); + } + }, + new FailureScenario( + ServerError.class, + DefaultNodeMetric.OTHER_ERRORS, + DefaultNodeMetric.RETRIES_ON_OTHER_ERROR, + DefaultNodeMetric.IGNORES_ON_OTHER_ERROR) { + @Override + public void mockRequestError(RequestHandlerTestHarness.Builder builder, Node node) { + builder.withResponse( + node, + defaultFrameOf( + new Error(ProtocolConstants.ErrorCode.SERVER_ERROR, "mock server error"))); + } + + @Override + public void mockRetryPolicyDecision(RetryPolicy policy, RetryDecision decision) { + when(policy.onErrorResponse(any(SimpleStatement.class), any(ServerError.class), eq(0))) + .thenReturn(decision); + } + }, + new FailureScenario( + HeartbeatException.class, + DefaultNodeMetric.ABORTED_REQUESTS, + DefaultNodeMetric.RETRIES_ON_ABORTED, + DefaultNodeMetric.IGNORES_ON_ABORTED) { + @Override + public void mockRequestError(RequestHandlerTestHarness.Builder builder, Node node) { + builder.withResponseFailure(node, mock(HeartbeatException.class)); + } + + @Override + public void mockRetryPolicyDecision(RetryPolicy policy, RetryDecision decision) { + when(policy.onRequestAborted( + any(SimpleStatement.class), any(HeartbeatException.class), eq(0))) + .thenReturn(decision); + } + }); + } + + @DataProvider + public static Object[][] failureAndIdempotent() { + return TestDataProviders.combine(failure(), idempotentConfig()); + } + + @DataProvider + public static Object[][] failureAndNotIdempotent() { + return TestDataProviders.combine(failure(), nonIdempotentConfig()); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerSpeculativeExecutionTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerSpeculativeExecutionTest.java new file mode 100644 index 00000000000..2eca70f1dc2 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerSpeculativeExecutionTest.java @@ -0,0 +1,422 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.AllNodesFailedException; +import com.datastax.oss.driver.api.core.NoNodeAvailableException; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import com.datastax.oss.driver.api.core.servererrors.BootstrappingException; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import com.datastax.oss.driver.internal.core.util.concurrent.CapturingTimer.CapturedTimeout; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.Error; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class CqlRequestHandlerSpeculativeExecutionTest extends CqlRequestHandlerTestBase { + + @Test + @UseDataProvider("nonIdempotentConfig") + public void should_not_schedule_speculative_executions_if_not_idempotent( + boolean defaultIdempotence, SimpleStatement statement) { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + SpeculativeExecutionPolicy speculativeExecutionPolicy = + harness.getContext().getSpeculativeExecutionPolicy(DriverExecutionProfile.DEFAULT_NAME); + + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test").handle(); + + node1Behavior.verifyWrite(); + + assertThat(harness.nextScheduledTimeout()).isNotNull(); // Discard the timeout task + assertThat(harness.nextScheduledTimeout()).isNull(); + + verifyNoMoreInteractions(speculativeExecutionPolicy); + verifyNoMoreInteractions(nodeMetricUpdater1); + } + } + + @Test + @UseDataProvider("idempotentConfig") + public void should_schedule_speculative_executions( + boolean defaultIdempotence, SimpleStatement statement) throws Exception { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + PoolBehavior node3Behavior = harnessBuilder.customBehavior(node3); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + SpeculativeExecutionPolicy speculativeExecutionPolicy = + harness.getContext().getSpeculativeExecutionPolicy(DriverExecutionProfile.DEFAULT_NAME); + long firstExecutionDelay = 100L; + long secondExecutionDelay = 200L; + when(speculativeExecutionPolicy.nextExecution( + any(Node.class), eq(null), eq(statement), eq(1))) + .thenReturn(firstExecutionDelay); + when(speculativeExecutionPolicy.nextExecution( + any(Node.class), eq(null), eq(statement), eq(2))) + .thenReturn(secondExecutionDelay); + when(speculativeExecutionPolicy.nextExecution( + any(Node.class), eq(null), eq(statement), eq(3))) + .thenReturn(-1L); + + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test").handle(); + + node1Behavior.verifyWrite(); + node1Behavior.setWriteSuccess(); + + harness.nextScheduledTimeout(); // Discard the timeout task + + CapturedTimeout speculativeExecution1 = harness.nextScheduledTimeout(); + assertThat(speculativeExecution1.getDelay(TimeUnit.MILLISECONDS)) + .isEqualTo(firstExecutionDelay); + verifyNoMoreInteractions(nodeMetricUpdater1); + speculativeExecution1.task().run(speculativeExecution1); + verify(nodeMetricUpdater1) + .incrementCounter( + DefaultNodeMetric.SPECULATIVE_EXECUTIONS, DriverExecutionProfile.DEFAULT_NAME); + node2Behavior.verifyWrite(); + node2Behavior.setWriteSuccess(); + + CapturedTimeout speculativeExecution2 = harness.nextScheduledTimeout(); + assertThat(speculativeExecution2.getDelay(TimeUnit.MILLISECONDS)) + .isEqualTo(secondExecutionDelay); + verifyNoMoreInteractions(nodeMetricUpdater2); + speculativeExecution2.task().run(speculativeExecution2); + verify(nodeMetricUpdater2) + .incrementCounter( + DefaultNodeMetric.SPECULATIVE_EXECUTIONS, DriverExecutionProfile.DEFAULT_NAME); + node3Behavior.verifyWrite(); + node3Behavior.setWriteSuccess(); + + // No more scheduled tasks since the policy returns 0 on the third call. + assertThat(harness.nextScheduledTimeout()).isNull(); + + // Note that we don't need to complete any response, the test is just about checking that + // executions are started. + } + } + + @Test + @UseDataProvider("idempotentConfig") + public void should_not_start_execution_if_result_complete( + boolean defaultIdempotence, SimpleStatement statement) throws Exception { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + SpeculativeExecutionPolicy speculativeExecutionPolicy = + harness.getContext().getSpeculativeExecutionPolicy(DriverExecutionProfile.DEFAULT_NAME); + long firstExecutionDelay = 100L; + when(speculativeExecutionPolicy.nextExecution( + any(Node.class), eq(null), eq(statement), eq(1))) + .thenReturn(firstExecutionDelay); + + CqlRequestHandler requestHandler = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test"); + CompletionStage resultSetFuture = requestHandler.handle(); + node1Behavior.verifyWrite(); + node1Behavior.setWriteSuccess(); + + harness.nextScheduledTimeout(); // Discard the timeout task + + // Check that the first execution was scheduled but don't run it yet + CapturedTimeout speculativeExecution1 = harness.nextScheduledTimeout(); + assertThat(speculativeExecution1.getDelay(TimeUnit.MILLISECONDS)) + .isEqualTo(firstExecutionDelay); + + // Complete the request from the initial execution + node1Behavior.setResponseSuccess(defaultFrameOf(singleRow())); + assertThatStage(resultSetFuture).isSuccess(); + + // Pending speculative executions should have been cancelled. However we don't check + // firstExecutionTask directly because the request handler's onResponse can sometimes be + // invoked before operationComplete (this is very unlikely in practice, but happens in our + // Travis CI build). When that happens, the speculative execution is not recorded yet when + // cancelScheduledTasks runs. + // So check the timeout future instead, since it's cancelled in the same method. + assertThat(requestHandler.scheduledTimeout.isCancelled()).isTrue(); + + // The fact that we missed the speculative execution is not a problem; even if it starts, it + // will eventually find out that the result is already complete and cancel itself: + speculativeExecution1.task().run(speculativeExecution1); + node2Behavior.verifyNoWrite(); + + verify(nodeMetricUpdater1) + .isEnabled(DefaultNodeMetric.CQL_MESSAGES, DriverExecutionProfile.DEFAULT_NAME); + verify(nodeMetricUpdater1) + .updateTimer( + eq(DefaultNodeMetric.CQL_MESSAGES), + eq(DriverExecutionProfile.DEFAULT_NAME), + anyLong(), + eq(TimeUnit.NANOSECONDS)); + verifyNoMoreInteractions(nodeMetricUpdater1); + } + } + + @Test + @UseDataProvider("idempotentConfig") + public void should_fail_if_no_nodes(boolean defaultIdempotence, SimpleStatement statement) { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + // No configured behaviors => will yield an empty query plan + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + SpeculativeExecutionPolicy speculativeExecutionPolicy = + harness.getContext().getSpeculativeExecutionPolicy(DriverExecutionProfile.DEFAULT_NAME); + long firstExecutionDelay = 100L; + when(speculativeExecutionPolicy.nextExecution( + any(Node.class), eq(null), eq(statement), eq(1))) + .thenReturn(firstExecutionDelay); + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + + harness.nextScheduledTimeout(); // Discard the timeout task + + assertThatStage(resultSetFuture) + .isFailed(error -> assertThat(error).isInstanceOf(NoNodeAvailableException.class)); + } + } + + @Test + @UseDataProvider("idempotentConfig") + public void should_fail_if_no_more_nodes_and_initial_execution_is_last( + boolean defaultIdempotence, SimpleStatement statement) throws Exception { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + harnessBuilder.withResponse( + node2, + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING, "mock message"))); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + SpeculativeExecutionPolicy speculativeExecutionPolicy = + harness.getContext().getSpeculativeExecutionPolicy(DriverExecutionProfile.DEFAULT_NAME); + long firstExecutionDelay = 100L; + when(speculativeExecutionPolicy.nextExecution( + any(Node.class), eq(null), eq(statement), eq(1))) + .thenReturn(firstExecutionDelay); + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + node1Behavior.verifyWrite(); + node1Behavior.setWriteSuccess(); + // do not simulate a response from node1 yet + + harness.nextScheduledTimeout(); // Discard the timeout task + + // Run the next scheduled task to start the speculative execution. node2 will reply with a + // BOOTSTRAPPING error, causing a RETRY_NEXT; but the query plan is now empty so the + // speculative execution stops. + // next scheduled timeout should be the first speculative execution. Get it and run it. + CapturedTimeout speculativeExecution1 = harness.nextScheduledTimeout(); + assertThat(speculativeExecution1.getDelay(TimeUnit.MILLISECONDS)) + .isEqualTo(firstExecutionDelay); + speculativeExecution1.task().run(speculativeExecution1); + + // node1 now replies with the same response, that triggers a RETRY_NEXT + node1Behavior.setResponseSuccess( + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING, "mock message"))); + + // But again the query plan is empty so that should fail the request + assertThatStage(resultSetFuture) + .isFailed( + error -> { + assertThat(error).isInstanceOf(AllNodesFailedException.class); + Map nodeErrors = ((AllNodesFailedException) error).getErrors(); + assertThat(nodeErrors).containsOnlyKeys(node1, node2); + assertThat(nodeErrors.get(node1)).isInstanceOf(BootstrappingException.class); + assertThat(nodeErrors.get(node2)).isInstanceOf(BootstrappingException.class); + }); + } + } + + @Test + @UseDataProvider("idempotentConfig") + public void should_fail_if_no_more_nodes_and_speculative_execution_is_last( + boolean defaultIdempotence, SimpleStatement statement) throws Exception { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + SpeculativeExecutionPolicy speculativeExecutionPolicy = + harness.getContext().getSpeculativeExecutionPolicy(DriverExecutionProfile.DEFAULT_NAME); + long firstExecutionDelay = 100L; + when(speculativeExecutionPolicy.nextExecution( + any(Node.class), eq(null), eq(statement), eq(1))) + .thenReturn(firstExecutionDelay); + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + node1Behavior.verifyWrite(); + node1Behavior.setWriteSuccess(); + // do not simulate a response from node1 yet + + harness.nextScheduledTimeout(); // Discard the timeout task + + // next scheduled timeout should be the first speculative execution. Get it and run it. + CapturedTimeout speculativeExecution1 = harness.nextScheduledTimeout(); + assertThat(speculativeExecution1.getDelay(TimeUnit.MILLISECONDS)) + .isEqualTo(firstExecutionDelay); + speculativeExecution1.task().run(speculativeExecution1); + + // node1 now replies with a BOOTSTRAPPING error that triggers a RETRY_NEXT + // but the query plan is empty so the initial execution stops + node1Behavior.setResponseSuccess( + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING, "mock message"))); + + // Same thing with node2, so the speculative execution should reach the end of the query plan + // and fail the request + node2Behavior.setResponseSuccess( + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING, "mock message"))); + + assertThatStage(resultSetFuture) + .isFailed( + error -> { + assertThat(error).isInstanceOf(AllNodesFailedException.class); + Map nodeErrors = ((AllNodesFailedException) error).getErrors(); + assertThat(nodeErrors).containsOnlyKeys(node1, node2); + assertThat(nodeErrors.get(node1)).isInstanceOf(BootstrappingException.class); + assertThat(nodeErrors.get(node2)).isInstanceOf(BootstrappingException.class); + }); + } + } + + @Test + @UseDataProvider("idempotentConfig") + public void should_retry_in_speculative_executions( + boolean defaultIdempotence, SimpleStatement statement) throws Exception { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + harnessBuilder.withResponse(node3, defaultFrameOf(singleRow())); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + SpeculativeExecutionPolicy speculativeExecutionPolicy = + harness.getContext().getSpeculativeExecutionPolicy(DriverExecutionProfile.DEFAULT_NAME); + long firstExecutionDelay = 100L; + when(speculativeExecutionPolicy.nextExecution( + any(Node.class), eq(null), eq(statement), eq(1))) + .thenReturn(firstExecutionDelay); + + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + node1Behavior.verifyWrite(); + node1Behavior.setWriteSuccess(); + // do not simulate a response from node1. The request will stay hanging for the rest of this + // test + + harness.nextScheduledTimeout(); // Discard the timeout task + + // next scheduled timeout should be the first speculative execution. Get it and run it. + CapturedTimeout speculativeExecution1 = harness.nextScheduledTimeout(); + assertThat(speculativeExecution1.getDelay(TimeUnit.MILLISECONDS)) + .isEqualTo(firstExecutionDelay); + speculativeExecution1.task().run(speculativeExecution1); + + node2Behavior.verifyWrite(); + node2Behavior.setWriteSuccess(); + + // node2 replies with a response that triggers a RETRY_NEXT + node2Behavior.setResponseSuccess( + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING, "mock message"))); + + // The second execution should move to node3 and complete the request + assertThatStage(resultSetFuture).isSuccess(); + + // The request to node1 was still in flight, it should have been cancelled + node1Behavior.verifyCancellation(); + } + } + + @Test + @UseDataProvider("idempotentConfig") + public void should_stop_retrying_other_executions_if_result_complete( + boolean defaultIdempotence, SimpleStatement statement) throws Exception { + RequestHandlerTestHarness.Builder harnessBuilder = + RequestHandlerTestHarness.builder().withDefaultIdempotence(defaultIdempotence); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + PoolBehavior node2Behavior = harnessBuilder.customBehavior(node2); + PoolBehavior node3Behavior = harnessBuilder.customBehavior(node3); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + SpeculativeExecutionPolicy speculativeExecutionPolicy = + harness.getContext().getSpeculativeExecutionPolicy(DriverExecutionProfile.DEFAULT_NAME); + long firstExecutionDelay = 100L; + when(speculativeExecutionPolicy.nextExecution( + any(Node.class), eq(null), eq(statement), eq(1))) + .thenReturn(firstExecutionDelay); + CompletionStage resultSetFuture = + new CqlRequestHandler(statement, harness.getSession(), harness.getContext(), "test") + .handle(); + node1Behavior.verifyWrite(); + node1Behavior.setWriteSuccess(); + + harness.nextScheduledTimeout(); // Discard the timeout task + + // next scheduled timeout should be the first speculative execution. Get it and run it. + CapturedTimeout speculativeExecution1 = harness.nextScheduledTimeout(); + assertThat(speculativeExecution1.getDelay(TimeUnit.MILLISECONDS)) + .isEqualTo(firstExecutionDelay); + speculativeExecution1.task().run(speculativeExecution1); + + node2Behavior.verifyWrite(); + node2Behavior.setWriteSuccess(); + + // Complete the request from the initial execution + node1Behavior.setResponseSuccess(defaultFrameOf(singleRow())); + assertThatStage(resultSetFuture).isSuccess(); + + // node2 replies with a response that would trigger a RETRY_NEXT if the request was still + // running + node2Behavior.setResponseSuccess( + defaultFrameOf(new Error(ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING, "mock message"))); + + // The speculative execution should not move to node3 because it is stopped + node3Behavior.verifyNoWrite(); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerTest.java new file mode 100644 index 00000000000..78542f4adb5 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerTest.java @@ -0,0 +1,207 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DriverTimeoutException; +import com.datastax.oss.driver.api.core.NoNodeAvailableException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.internal.core.session.RepreparePayload; +import com.datastax.oss.driver.internal.core.util.concurrent.CapturingTimer.CapturedTimeout; +import com.datastax.oss.protocol.internal.request.Prepare; +import com.datastax.oss.protocol.internal.response.error.Unprepared; +import com.datastax.oss.protocol.internal.response.result.Prepared; +import com.datastax.oss.protocol.internal.response.result.SetKeyspace; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Collections; +import java.util.Iterator; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class CqlRequestHandlerTest extends CqlRequestHandlerTestBase { + + @Test + public void should_complete_result_if_first_node_replies_immediately() { + try (RequestHandlerTestHarness harness = + RequestHandlerTestHarness.builder() + .withResponse(node1, defaultFrameOf(singleRow())) + .build()) { + + CompletionStage resultSetFuture = + new CqlRequestHandler( + UNDEFINED_IDEMPOTENCE_STATEMENT, + harness.getSession(), + harness.getContext(), + "test") + .handle(); + + assertThatStage(resultSetFuture) + .isSuccess( + resultSet -> { + Iterator rows = resultSet.currentPage().iterator(); + assertThat(rows.hasNext()).isTrue(); + assertThat(rows.next().getString("message")).isEqualTo("hello, world"); + + ExecutionInfo executionInfo = resultSet.getExecutionInfo(); + assertThat(executionInfo.getCoordinator()).isEqualTo(node1); + assertThat(executionInfo.getErrors()).isEmpty(); + assertThat(executionInfo.getIncomingPayload()).isEmpty(); + assertThat(executionInfo.getPagingState()).isNull(); + assertThat(executionInfo.getSpeculativeExecutionCount()).isEqualTo(0); + assertThat(executionInfo.getSuccessfulExecutionIndex()).isEqualTo(0); + assertThat(executionInfo.getWarnings()).isEmpty(); + }); + } + } + + @Test + public void should_fail_if_no_node_available() { + try (RequestHandlerTestHarness harness = + RequestHandlerTestHarness.builder() + // Mock no responses => this will produce an empty query plan + .build()) { + + CompletionStage resultSetFuture = + new CqlRequestHandler( + UNDEFINED_IDEMPOTENCE_STATEMENT, + harness.getSession(), + harness.getContext(), + "test") + .handle(); + + assertThatStage(resultSetFuture) + .isFailed(error -> assertThat(error).isInstanceOf(NoNodeAvailableException.class)); + } + } + + @Test + public void should_time_out_if_first_node_takes_too_long_to_respond() throws Exception { + RequestHandlerTestHarness.Builder harnessBuilder = RequestHandlerTestHarness.builder(); + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + node1Behavior.setWriteSuccess(); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + + CompletionStage resultSetFuture = + new CqlRequestHandler( + UNDEFINED_IDEMPOTENCE_STATEMENT, + harness.getSession(), + harness.getContext(), + "test") + .handle(); + + // First scheduled task is the timeout, run it before node1 has responded + CapturedTimeout requestTimeout = harness.nextScheduledTimeout(); + Duration configuredTimeoutDuration = + harness + .getContext() + .getConfig() + .getDefaultProfile() + .getDuration(DefaultDriverOption.REQUEST_TIMEOUT); + assertThat(requestTimeout.getDelay(TimeUnit.NANOSECONDS)) + .isEqualTo(configuredTimeoutDuration.toNanos()); + requestTimeout.task().run(requestTimeout); + + assertThatStage(resultSetFuture) + .isFailed(t -> assertThat(t).isInstanceOf(DriverTimeoutException.class)); + } + } + + @Test + public void should_switch_keyspace_on_session_after_successful_use_statement() { + try (RequestHandlerTestHarness harness = + RequestHandlerTestHarness.builder() + .withResponse(node1, defaultFrameOf(new SetKeyspace("newKeyspace"))) + .build()) { + + CompletionStage resultSetFuture = + new CqlRequestHandler( + UNDEFINED_IDEMPOTENCE_STATEMENT, + harness.getSession(), + harness.getContext(), + "test") + .handle(); + + assertThatStage(resultSetFuture) + .isSuccess( + resultSet -> + verify(harness.getSession()) + .setKeyspace(CqlIdentifier.fromInternal("newKeyspace"))); + } + } + + @Test + public void should_reprepare_on_the_fly_if_not_prepared() throws InterruptedException { + ByteBuffer mockId = Bytes.fromHexString("0xffff"); + + PreparedStatement preparedStatement = mock(PreparedStatement.class); + when(preparedStatement.getId()).thenReturn(mockId); + ColumnDefinitions columnDefinitions = mock(ColumnDefinitions.class); + when(columnDefinitions.size()).thenReturn(0); + when(preparedStatement.getResultSetDefinitions()).thenReturn(columnDefinitions); + BoundStatement boundStatement = mock(BoundStatement.class); + when(boundStatement.getPreparedStatement()).thenReturn(preparedStatement); + when(boundStatement.getValues()).thenReturn(Collections.emptyList()); + + RequestHandlerTestHarness.Builder harnessBuilder = RequestHandlerTestHarness.builder(); + // For the first attempt that gets the UNPREPARED response + PoolBehavior node1Behavior = harnessBuilder.customBehavior(node1); + // For the second attempt that succeeds + harnessBuilder.withResponse(node1, defaultFrameOf(singleRow())); + + try (RequestHandlerTestHarness harness = harnessBuilder.build()) { + + // The handler will look for the info to reprepare in the session's cache, put it there + ConcurrentMap repreparePayloads = new ConcurrentHashMap<>(); + repreparePayloads.put( + mockId, new RepreparePayload(mockId, "mock query", null, Collections.emptyMap())); + when(harness.getSession().getRepreparePayloads()).thenReturn(repreparePayloads); + + CompletionStage resultSetFuture = + new CqlRequestHandler(boundStatement, harness.getSession(), harness.getContext(), "test") + .handle(); + + // Before we proceed, mock the PREPARE exchange that will occur as soon as we complete the + // first response. + node1Behavior.mockFollowupRequest( + Prepare.class, defaultFrameOf(new Prepared(mockId.array(), null, null, null))); + + node1Behavior.setWriteSuccess(); + node1Behavior.setResponseSuccess( + defaultFrameOf(new Unprepared("mock message", mockId.array()))); + + // Should now re-prepare, re-execute and succeed. + assertThatStage(resultSetFuture).isSuccess(); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerTestBase.java new file mode 100644 index 00000000000..54fb1e3a7b3 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerTestBase.java @@ -0,0 +1,141 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.TestDataProviders; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.metrics.NodeMetric; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.metrics.NodeMetricUpdater; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.result.ColumnSpec; +import com.datastax.oss.protocol.internal.response.result.DefaultRows; +import com.datastax.oss.protocol.internal.response.result.RawType; +import com.datastax.oss.protocol.internal.response.result.RowsMetadata; +import com.datastax.oss.protocol.internal.util.Bytes; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.List; +import java.util.Queue; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(DataProviderRunner.class) +public abstract class CqlRequestHandlerTestBase { + + protected static final SimpleStatement UNDEFINED_IDEMPOTENCE_STATEMENT = + SimpleStatement.newInstance("mock query"); + protected static final SimpleStatement IDEMPOTENT_STATEMENT = + SimpleStatement.builder("mock query").setIdempotence(true).build(); + protected static final SimpleStatement NON_IDEMPOTENT_STATEMENT = + SimpleStatement.builder("mock query").setIdempotence(false).build(); + protected static final InetSocketAddress ADDRESS1 = new InetSocketAddress("127.0.0.1", 9042); + protected static final InetSocketAddress ADDRESS2 = new InetSocketAddress("127.0.0.2", 9042); + protected static final InetSocketAddress ADDRESS3 = new InetSocketAddress("127.0.0.3", 9042); + + @Mock protected DefaultNode node1; + @Mock protected DefaultNode node2; + @Mock protected DefaultNode node3; + @Mock protected NodeMetricUpdater nodeMetricUpdater1; + @Mock protected NodeMetricUpdater nodeMetricUpdater2; + @Mock protected NodeMetricUpdater nodeMetricUpdater3; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(node1.getMetricUpdater()).thenReturn(nodeMetricUpdater1); + when(nodeMetricUpdater1.isEnabled(any(NodeMetric.class), anyString())).thenReturn(true); + when(node2.getMetricUpdater()).thenReturn(nodeMetricUpdater2); + when(nodeMetricUpdater2.isEnabled(any(NodeMetric.class), anyString())).thenReturn(true); + when(node3.getMetricUpdater()).thenReturn(nodeMetricUpdater3); + when(nodeMetricUpdater3.isEnabled(any(NodeMetric.class), anyString())).thenReturn(true); + } + + protected static Frame defaultFrameOf(Message responseMessage) { + return Frame.forResponse( + DefaultProtocolVersion.V4.getCode(), + 0, + null, + Frame.NO_PAYLOAD, + Collections.emptyList(), + responseMessage); + } + + // Returns a single row, with a single "message" column with the value "hello, world" + protected static Message singleRow() { + RowsMetadata metadata = + new RowsMetadata( + ImmutableList.of( + new ColumnSpec( + "ks", + "table", + "message", + 0, + RawType.PRIMITIVES.get(ProtocolConstants.DataType.VARCHAR))), + null, + new int[] {}, + null); + Queue> data = new ArrayDeque<>(); + data.add(ImmutableList.of(Bytes.fromHexString("0x68656C6C6F2C20776F726C64"))); + return new DefaultRows(metadata, data); + } + + /** + * The combination of the default idempotence option and statement setting that produce an + * idempotent statement. + */ + @DataProvider + public static Object[][] idempotentConfig() { + return new Object[][] { + new Object[] {true, UNDEFINED_IDEMPOTENCE_STATEMENT}, + new Object[] {false, IDEMPOTENT_STATEMENT}, + new Object[] {true, IDEMPOTENT_STATEMENT}, + }; + } + + /** + * The combination of the default idempotence option and statement setting that produce a non + * idempotent statement. + */ + @DataProvider + public static Object[][] nonIdempotentConfig() { + return new Object[][] { + new Object[] {false, UNDEFINED_IDEMPOTENCE_STATEMENT}, + new Object[] {true, NON_IDEMPOTENT_STATEMENT}, + new Object[] {false, NON_IDEMPOTENT_STATEMENT}, + }; + } + + @DataProvider + public static Object[][] allIdempotenceConfigs() { + return TestDataProviders.concat(idempotentConfig(), nonIdempotentConfig()); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerTrackerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerTrackerTest.java new file mode 100644 index 00000000000..330c6bff50d --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/CqlRequestHandlerTrackerTest.java @@ -0,0 +1,117 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.servererrors.BootstrappingException; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import com.datastax.oss.driver.internal.core.tracker.NoopRequestTracker; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.Error; +import java.util.concurrent.CompletionStage; +import org.junit.Test; + +public class CqlRequestHandlerTrackerTest extends CqlRequestHandlerTestBase { + + @Test + public void should_invoke_request_tracker() { + try (RequestHandlerTestHarness harness = + RequestHandlerTestHarness.builder() + .withDefaultIdempotence(true) + .withResponse( + node1, + defaultFrameOf( + new Error(ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING, "mock message"))) + .withResponse(node2, defaultFrameOf(singleRow())) + .build()) { + + RequestTracker requestTracker = mock(RequestTracker.class); + when(harness.getContext().getRequestTracker()).thenReturn(requestTracker); + + CompletionStage resultSetFuture = + new CqlRequestHandler( + UNDEFINED_IDEMPOTENCE_STATEMENT, + harness.getSession(), + harness.getContext(), + "test") + .handle(); + + assertThatStage(resultSetFuture) + .isSuccess( + resultSet -> { + verify(requestTracker) + .onNodeError( + eq(UNDEFINED_IDEMPOTENCE_STATEMENT), + any(BootstrappingException.class), + anyLong(), + any(DriverExecutionProfile.class), + eq(node1)); + verify(requestTracker) + .onNodeSuccess( + eq(UNDEFINED_IDEMPOTENCE_STATEMENT), + anyLong(), + any(DriverExecutionProfile.class), + eq(node2)); + verify(requestTracker) + .onSuccess( + eq(UNDEFINED_IDEMPOTENCE_STATEMENT), + anyLong(), + any(DriverExecutionProfile.class), + eq(node2)); + verifyNoMoreInteractions(requestTracker); + }); + } + } + + @Test + public void should_not_invoke_noop_request_tracker() { + try (RequestHandlerTestHarness harness = + RequestHandlerTestHarness.builder() + .withDefaultIdempotence(true) + .withResponse( + node1, + defaultFrameOf( + new Error(ProtocolConstants.ErrorCode.IS_BOOTSTRAPPING, "mock message"))) + .withResponse(node2, defaultFrameOf(singleRow())) + .build()) { + + RequestTracker requestTracker = spy(new NoopRequestTracker(harness.getContext())); + when(harness.getContext().getRequestTracker()).thenReturn(requestTracker); + + CompletionStage resultSetFuture = + new CqlRequestHandler( + UNDEFINED_IDEMPOTENCE_STATEMENT, + harness.getSession(), + harness.getContext(), + "test") + .handle(); + + assertThatStage(resultSetFuture) + .isSuccess(resultSet -> verifyNoMoreInteractions(requestTracker)); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/DefaultAsyncResultSetTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/DefaultAsyncResultSetTest.java new file mode 100644 index 00000000000..ebd9a6d0f0d --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/DefaultAsyncResultSetTest.java @@ -0,0 +1,193 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class DefaultAsyncResultSetTest { + + @Mock private ColumnDefinitions columnDefinitions; + @Mock private ExecutionInfo executionInfo; + @Mock private Statement statement; + @Mock private CqlSession session; + @Mock private InternalDriverContext context; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(executionInfo.getStatement()).thenAnswer(invocation -> statement); + when(context.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT); + when(context.getProtocolVersion()).thenReturn(DefaultProtocolVersion.DEFAULT); + } + + @Test(expected = IllegalStateException.class) + public void should_fail_to_fetch_next_page_if_last() { + // Given + when(executionInfo.getPagingState()).thenReturn(null); + + // When + DefaultAsyncResultSet resultSet = + new DefaultAsyncResultSet( + columnDefinitions, executionInfo, new ArrayDeque<>(), session, context); + + // Then + assertThat(resultSet.hasMorePages()).isFalse(); + resultSet.fetchNextPage(); + } + + @Test + public void should_invoke_session_to_fetch_next_page() { + // Given + ByteBuffer mockPagingState = ByteBuffer.allocate(0); + when(executionInfo.getPagingState()).thenReturn(mockPagingState); + + Statement mockNextStatement = mock(Statement.class); + when(((Statement) statement).copy(mockPagingState)).thenReturn(mockNextStatement); + + CompletableFuture mockResultFuture = new CompletableFuture<>(); + when(session.executeAsync(any(Statement.class))).thenAnswer(invocation -> mockResultFuture); + + // When + DefaultAsyncResultSet resultSet = + new DefaultAsyncResultSet( + columnDefinitions, executionInfo, new ArrayDeque<>(), session, context); + assertThat(resultSet.hasMorePages()).isTrue(); + CompletionStage nextPageFuture = resultSet.fetchNextPage(); + + // Then + verify(statement).copy(mockPagingState); + verify(session).executeAsync(mockNextStatement); + assertThatStage(nextPageFuture).isEqualTo(mockResultFuture); + } + + @Test + public void should_report_applied_if_column_not_present_and_empty() { + // Given + when(columnDefinitions.contains("[applied]")).thenReturn(false); + + // When + DefaultAsyncResultSet resultSet = + new DefaultAsyncResultSet( + columnDefinitions, executionInfo, new ArrayDeque<>(), session, context); + + // Then + assertThat(resultSet.wasApplied()).isTrue(); + } + + @Test + public void should_report_applied_if_column_not_present_and_not_empty() { + // Given + when(columnDefinitions.contains("[applied]")).thenReturn(false); + Queue> data = new ArrayDeque<>(); + data.add(Lists.newArrayList(Bytes.fromHexString("0xffff"))); + + // When + DefaultAsyncResultSet resultSet = + new DefaultAsyncResultSet(columnDefinitions, executionInfo, data, session, context); + + // Then + assertThat(resultSet.wasApplied()).isTrue(); + } + + @Test + public void should_report_not_applied_if_column_present_and_false() { + // Given + when(columnDefinitions.contains("[applied]")).thenReturn(true); + ColumnDefinition columnDefinition = mock(ColumnDefinition.class); + when(columnDefinition.getType()).thenReturn(DataTypes.BOOLEAN); + when(columnDefinitions.get("[applied]")).thenReturn(columnDefinition); + when(columnDefinitions.firstIndexOf("[applied]")).thenReturn(0); + when(columnDefinitions.get(0)).thenReturn(columnDefinition); + + Queue> data = new ArrayDeque<>(); + data.add(Lists.newArrayList(TypeCodecs.BOOLEAN.encode(false, DefaultProtocolVersion.DEFAULT))); + + // When + DefaultAsyncResultSet resultSet = + new DefaultAsyncResultSet(columnDefinitions, executionInfo, data, session, context); + + // Then + assertThat(resultSet.wasApplied()).isFalse(); + } + + @Test + public void should_report_not_applied_if_column_present_and_true() { + // Given + when(columnDefinitions.contains("[applied]")).thenReturn(true); + ColumnDefinition columnDefinition = mock(ColumnDefinition.class); + when(columnDefinition.getType()).thenReturn(DataTypes.BOOLEAN); + when(columnDefinitions.get("[applied]")).thenReturn(columnDefinition); + when(columnDefinitions.firstIndexOf("[applied]")).thenReturn(0); + when(columnDefinitions.get(0)).thenReturn(columnDefinition); + + Queue> data = new ArrayDeque<>(); + data.add(Lists.newArrayList(TypeCodecs.BOOLEAN.encode(true, DefaultProtocolVersion.DEFAULT))); + + // When + DefaultAsyncResultSet resultSet = + new DefaultAsyncResultSet(columnDefinitions, executionInfo, data, session, context); + + // Then + assertThat(resultSet.wasApplied()).isTrue(); + } + + @Test(expected = IllegalStateException.class) + public void should_fail_to_report_if_applied_if_column_present_but_empty() { + // Given + when(columnDefinitions.contains("[applied]")).thenReturn(true); + ColumnDefinition columnDefinition = mock(ColumnDefinition.class); + when(columnDefinition.getType()).thenReturn(DataTypes.BOOLEAN); + when(columnDefinitions.get("[applied]")).thenReturn(columnDefinition); + + // When + DefaultAsyncResultSet resultSet = + new DefaultAsyncResultSet( + columnDefinitions, executionInfo, new ArrayDeque<>(), session, context); + + // Then + resultSet.wasApplied(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/PoolBehavior.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/PoolBehavior.java new file mode 100644 index 00000000000..55594f46aed --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/PoolBehavior.java @@ -0,0 +1,128 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.ResponseCallback; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.Message; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.EventLoop; +import io.netty.channel.socket.DefaultSocketChannelConfig; +import io.netty.util.concurrent.GlobalEventExecutor; +import io.netty.util.concurrent.Promise; +import java.util.concurrent.CompletableFuture; + +/** + * The simulated behavior of the connection pool for a given node in a {@link + * RequestHandlerTestHarness}. + * + *

This only covers a single attempt, if the node is to be tried multiple times there will be + * multiple instances of this class. + */ +public class PoolBehavior { + + final Node node; + final DriverChannel channel; + private final Promise writePromise; + private final CompletableFuture callbackFuture = new CompletableFuture<>(); + + public PoolBehavior(Node node, boolean createChannel) { + this.node = node; + if (!createChannel) { + this.channel = null; + this.writePromise = null; + } else { + this.channel = mock(DriverChannel.class); + EventLoop eventLoop = mock(EventLoop.class); + ChannelConfig config = mock(DefaultSocketChannelConfig.class); + this.writePromise = GlobalEventExecutor.INSTANCE.newPromise(); + when(channel.write(any(Message.class), anyBoolean(), anyMap(), any(ResponseCallback.class))) + .thenAnswer( + invocation -> { + ResponseCallback callback = invocation.getArgument(3); + callback.onStreamIdAssigned(1); + callbackFuture.complete(callback); + return writePromise; + }); + ChannelFuture closeFuture = mock(ChannelFuture.class); + when(channel.closeFuture()).thenReturn(closeFuture); + when(channel.eventLoop()).thenReturn(eventLoop); + when(channel.config()).thenReturn(config); + } + } + + public void verifyWrite() { + verify(channel).write(any(Message.class), anyBoolean(), anyMap(), any(ResponseCallback.class)); + } + + public void verifyNoWrite() { + verify(channel, never()) + .write(any(Message.class), anyBoolean(), anyMap(), any(ResponseCallback.class)); + } + + public void setWriteSuccess() { + writePromise.setSuccess(null); + } + + public void setWriteFailure(Throwable cause) { + writePromise.setFailure(cause); + } + + public void setResponseSuccess(Frame responseFrame) { + callbackFuture.thenAccept(callback -> callback.onResponse(responseFrame)); + } + + public void setResponseFailure(Throwable cause) { + callbackFuture.thenAccept(callback -> callback.onFailure(cause)); + } + + public Node getNode() { + return node; + } + + public DriverChannel getChannel() { + return channel; + } + + /** Mocks a follow-up request on the same channel. */ + public void mockFollowupRequest(Class expectedMessage, Frame responseFrame) { + Promise writePromise2 = GlobalEventExecutor.INSTANCE.newPromise(); + CompletableFuture callbackFuture2 = new CompletableFuture<>(); + when(channel.write(any(expectedMessage), anyBoolean(), anyMap(), any(ResponseCallback.class))) + .thenAnswer( + invocation -> { + callbackFuture2.complete(invocation.getArgument(3)); + return writePromise2; + }); + writePromise2.setSuccess(null); + callbackFuture2.thenAccept(callback -> callback.onResponse(responseFrame)); + } + + public void verifyCancellation() { + verify(channel).cancel(any(ResponseCallback.class)); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/QueryTraceFetcherTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/QueryTraceFetcherTest.java new file mode 100644 index 00000000000..f84abfe39f7 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/QueryTraceFetcherTest.java @@ -0,0 +1,379 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.QueryTrace; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.TraceEvent; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.protocol.internal.util.Bytes; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.EventExecutorGroup; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class QueryTraceFetcherTest { + + private static final UUID TRACING_ID = UUID.randomUUID(); + private static final ByteBuffer PAGING_STATE = Bytes.fromHexString("0xdeadbeef"); + + @Mock private CqlSession session; + @Mock private InternalDriverContext context; + @Mock private DriverExecutionProfile config; + @Mock private DriverExecutionProfile traceConfig; + @Mock private NettyOptions nettyOptions; + @Mock private EventExecutorGroup adminEventExecutorGroup; + @Mock private EventExecutor eventExecutor; + @Mock private InetAddress address; + + @Captor private ArgumentCaptor statementCaptor; + + @Before + public void setup() { + when(context.getNettyOptions()).thenReturn(nettyOptions); + when(nettyOptions.adminEventExecutorGroup()).thenReturn(adminEventExecutorGroup); + when(adminEventExecutorGroup.next()).thenReturn(eventExecutor); + // Always execute scheduled tasks immediately: + when(eventExecutor.schedule(any(Runnable.class), anyLong(), any(TimeUnit.class))) + .thenAnswer( + invocation -> { + Runnable runnable = invocation.getArgument(0); + runnable.run(); + // OK because the production code doesn't use the result: + return null; + }); + + when(config.getInt(DefaultDriverOption.REQUEST_TRACE_ATTEMPTS)).thenReturn(3); + // Doesn't really matter since we mock the scheduler + when(config.getDuration(DefaultDriverOption.REQUEST_TRACE_INTERVAL)).thenReturn(Duration.ZERO); + when(config.getString(DefaultDriverOption.REQUEST_CONSISTENCY)) + .thenReturn(DefaultConsistencyLevel.LOCAL_ONE.name()); + when(config.getString(DefaultDriverOption.REQUEST_TRACE_CONSISTENCY)) + .thenReturn(DefaultConsistencyLevel.ONE.name()); + + when(config.withString( + DefaultDriverOption.REQUEST_CONSISTENCY, DefaultConsistencyLevel.ONE.name())) + .thenReturn(traceConfig); + } + + @Test + public void should_succeed_when_both_queries_succeed_immediately() { + // Given + CompletionStage sessionRow = completeSessionRow(); + CompletionStage eventRows = singlePageEventRows(); + when(session.executeAsync(any(SimpleStatement.class))) + .thenAnswer(invocation -> sessionRow) + .thenAnswer(invocation -> eventRows); + + // When + QueryTraceFetcher fetcher = new QueryTraceFetcher(TRACING_ID, session, context, config); + CompletionStage traceFuture = fetcher.fetch(); + + // Then + verify(session, times(2)).executeAsync(statementCaptor.capture()); + List statements = statementCaptor.getAllValues(); + assertSessionQuery(statements.get(0)); + SimpleStatement statement = statements.get(1); + assertEventsQuery(statement); + verifyNoMoreInteractions(session); + + assertThatStage(traceFuture) + .isSuccess( + trace -> { + assertThat(trace.getTracingId()).isEqualTo(TRACING_ID); + assertThat(trace.getRequestType()).isEqualTo("mock request"); + assertThat(trace.getDurationMicros()).isEqualTo(42); + assertThat(trace.getCoordinator()).isEqualTo(address); + assertThat(trace.getParameters()) + .hasSize(2) + .containsEntry("key1", "value1") + .containsEntry("key2", "value2"); + assertThat(trace.getStartedAt()).isEqualTo(0); + + List events = trace.getEvents(); + assertThat(events).hasSize(3); + for (int i = 0; i < events.size(); i++) { + TraceEvent event = events.get(i); + assertThat(event.getActivity()).isEqualTo("mock activity " + i); + assertThat(event.getTimestamp()).isEqualTo(i); + assertThat(event.getSource()).isEqualTo(address); + assertThat(event.getSourceElapsedMicros()).isEqualTo(i); + assertThat(event.getThreadName()).isEqualTo("mock thread " + i); + } + }); + } + + /** + * This should not happen with a sane configuration, but we need to handle it in case {@link + * DefaultDriverOption#REQUEST_PAGE_SIZE} is set ridiculously low. + */ + @Test + public void should_succeed_when_events_query_is_paged() { + // Given + CompletionStage sessionRow = completeSessionRow(); + CompletionStage eventRows1 = multiPageEventRows1(); + CompletionStage eventRows2 = multiPageEventRows2(); + when(session.executeAsync(any(SimpleStatement.class))) + .thenAnswer(invocation -> sessionRow) + .thenAnswer(invocation -> eventRows1) + .thenAnswer(invocation -> eventRows2); + + // When + QueryTraceFetcher fetcher = new QueryTraceFetcher(TRACING_ID, session, context, config); + CompletionStage traceFuture = fetcher.fetch(); + + // Then + verify(session, times(3)).executeAsync(statementCaptor.capture()); + List statements = statementCaptor.getAllValues(); + assertSessionQuery(statements.get(0)); + assertEventsQuery(statements.get(1)); + assertEventsQuery(statements.get(2)); + assertThat(statements.get(2).getPagingState()).isEqualTo(PAGING_STATE); + verifyNoMoreInteractions(session); + + assertThatStage(traceFuture).isSuccess(trace -> assertThat(trace.getEvents()).hasSize(2)); + } + + @Test + public void should_retry_when_session_row_is_incomplete() { + // Given + CompletionStage sessionRow1 = incompleteSessionRow(); + CompletionStage sessionRow2 = completeSessionRow(); + CompletionStage eventRows = singlePageEventRows(); + when(session.executeAsync(any(SimpleStatement.class))) + .thenAnswer(invocation -> sessionRow1) + .thenAnswer(invocation -> sessionRow2) + .thenAnswer(invocation -> eventRows); + + // When + QueryTraceFetcher fetcher = new QueryTraceFetcher(TRACING_ID, session, context, config); + CompletionStage traceFuture = fetcher.fetch(); + + // Then + verify(session, times(3)).executeAsync(statementCaptor.capture()); + List statements = statementCaptor.getAllValues(); + assertSessionQuery(statements.get(0)); + assertSessionQuery(statements.get(1)); + assertEventsQuery(statements.get(2)); + verifyNoMoreInteractions(session); + + assertThatStage(traceFuture) + .isSuccess( + trace -> { + assertThat(trace.getTracingId()).isEqualTo(TRACING_ID); + assertThat(trace.getRequestType()).isEqualTo("mock request"); + assertThat(trace.getDurationMicros()).isEqualTo(42); + assertThat(trace.getCoordinator()).isEqualTo(address); + assertThat(trace.getParameters()) + .hasSize(2) + .containsEntry("key1", "value1") + .containsEntry("key2", "value2"); + assertThat(trace.getStartedAt()).isEqualTo(0); + + List events = trace.getEvents(); + assertThat(events).hasSize(3); + for (int i = 0; i < events.size(); i++) { + TraceEvent event = events.get(i); + assertThat(event.getActivity()).isEqualTo("mock activity " + i); + assertThat(event.getTimestamp()).isEqualTo(i); + assertThat(event.getSource()).isEqualTo(address); + assertThat(event.getSourceElapsedMicros()).isEqualTo(i); + assertThat(event.getThreadName()).isEqualTo("mock thread " + i); + } + }); + } + + @Test + public void should_fail_when_session_query_fails() { + // Given + RuntimeException mockError = new RuntimeException("mock error"); + when(session.executeAsync(any(SimpleStatement.class))) + .thenReturn(CompletableFutures.failedFuture(mockError)); + + // When + QueryTraceFetcher fetcher = new QueryTraceFetcher(TRACING_ID, session, context, config); + CompletionStage traceFuture = fetcher.fetch(); + + // Then + verify(session).executeAsync(statementCaptor.capture()); + SimpleStatement statement = statementCaptor.getValue(); + assertSessionQuery(statement); + verifyNoMoreInteractions(session); + + assertThatStage(traceFuture).isFailed(error -> assertThat(error).isSameAs(mockError)); + } + + @Test + public void should_fail_when_session_query_still_incomplete_after_max_tries() { + // Given + CompletionStage sessionRow1 = incompleteSessionRow(); + CompletionStage sessionRow2 = incompleteSessionRow(); + CompletionStage sessionRow3 = incompleteSessionRow(); + when(session.executeAsync(any(SimpleStatement.class))) + .thenAnswer(invocation -> sessionRow1) + .thenAnswer(invocation -> sessionRow2) + .thenAnswer(invocation -> sessionRow3); + + // When + QueryTraceFetcher fetcher = new QueryTraceFetcher(TRACING_ID, session, context, config); + CompletionStage traceFuture = fetcher.fetch(); + + // Then + verify(session, times(3)).executeAsync(statementCaptor.capture()); + List statements = statementCaptor.getAllValues(); + for (int i = 0; i < 3; i++) { + assertSessionQuery(statements.get(i)); + } + + assertThatStage(traceFuture) + .isFailed( + error -> + assertThat(error.getMessage()) + .isEqualTo( + String.format("Trace %s still not complete after 3 attempts", TRACING_ID))); + } + + private CompletionStage completeSessionRow() { + return sessionRow(42); + } + + private CompletionStage incompleteSessionRow() { + return sessionRow(null); + } + + private CompletionStage sessionRow(Integer duration) { + Row row = mock(Row.class); + when(row.getString("request")).thenReturn("mock request"); + if (duration == null) { + when(row.isNull("duration")).thenReturn(true); + } else { + when(row.getInt("duration")).thenReturn(duration); + } + when(row.getInetAddress("coordinator")).thenReturn(address); + when(row.getMap("parameters", String.class, String.class)) + .thenReturn(ImmutableMap.of("key1", "value1", "key2", "value2")); + when(row.isNull("started_at")).thenReturn(false); + when(row.getInstant("started_at")).thenReturn(Instant.EPOCH); + + AsyncResultSet rs = mock(AsyncResultSet.class); + when(rs.one()).thenReturn(row); + return CompletableFuture.completedFuture(rs); + } + + private CompletionStage singlePageEventRows() { + List rows = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + rows.add(eventRow(i)); + } + + AsyncResultSet rs = mock(AsyncResultSet.class); + when(rs.currentPage()).thenReturn(rows); + + ExecutionInfo executionInfo = mock(ExecutionInfo.class); + when(executionInfo.getPagingState()).thenReturn(null); + when(rs.getExecutionInfo()).thenReturn(executionInfo); + + return CompletableFuture.completedFuture(rs); + } + + private CompletionStage multiPageEventRows1() { + AsyncResultSet rs = mock(AsyncResultSet.class); + + ImmutableList rows = ImmutableList.of(eventRow(0)); + when(rs.currentPage()).thenReturn(rows); + + ExecutionInfo executionInfo = mock(ExecutionInfo.class); + when(executionInfo.getPagingState()).thenReturn(PAGING_STATE); + when(rs.getExecutionInfo()).thenReturn(executionInfo); + + return CompletableFuture.completedFuture(rs); + } + + private CompletionStage multiPageEventRows2() { + AsyncResultSet rs = mock(AsyncResultSet.class); + + ImmutableList rows = ImmutableList.of(eventRow(1)); + when(rs.currentPage()).thenReturn(rows); + + ExecutionInfo executionInfo = mock(ExecutionInfo.class); + when(executionInfo.getPagingState()).thenReturn(null); + when(rs.getExecutionInfo()).thenReturn(executionInfo); + + return CompletableFuture.completedFuture(rs); + } + + private Row eventRow(int i) { + Row row = mock(Row.class); + when(row.getString("activity")).thenReturn("mock activity " + i); + when(row.getUuid("event_id")).thenReturn(Uuids.startOf(i)); + when(row.getInetAddress("source")).thenReturn(address); + when(row.getInt("source_elapsed")).thenReturn(i); + when(row.getString("thread")).thenReturn("mock thread " + i); + return row; + } + + private void assertSessionQuery(SimpleStatement statement) { + assertThat(statement.getQuery()) + .isEqualTo("SELECT * FROM system_traces.sessions WHERE session_id = ?"); + assertThat(statement.getPositionalValues()).containsOnly(TRACING_ID); + assertThat(statement.getExecutionProfile()).isEqualTo(traceConfig); + } + + private void assertEventsQuery(SimpleStatement statement) { + assertThat(statement.getQuery()) + .isEqualTo("SELECT * FROM system_traces.events WHERE session_id = ?"); + assertThat(statement.getPositionalValues()).containsOnly(TRACING_ID); + assertThat(statement.getExecutionProfile()).isEqualTo(traceConfig); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/RequestHandlerTestHarness.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/RequestHandlerTestHarness.java new file mode 100644 index 00000000000..11ba0cd5f55 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/RequestHandlerTestHarness.java @@ -0,0 +1,312 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DefaultConsistencyLevel; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metrics.SessionMetric; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import com.datastax.oss.driver.api.core.time.TimestampGenerator; +import com.datastax.oss.driver.internal.core.DefaultConsistencyLevelRegistry; +import com.datastax.oss.driver.internal.core.ProtocolFeature; +import com.datastax.oss.driver.internal.core.ProtocolVersionRegistry; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.metadata.DefaultMetadata; +import com.datastax.oss.driver.internal.core.metadata.LoadBalancingPolicyWrapper; +import com.datastax.oss.driver.internal.core.metrics.SessionMetricUpdater; +import com.datastax.oss.driver.internal.core.pool.ChannelPool; +import com.datastax.oss.driver.internal.core.servererrors.DefaultWriteTypeRegistry; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.internal.core.session.throttling.PassThroughRequestThrottler; +import com.datastax.oss.driver.internal.core.tracker.NoopRequestTracker; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; +import com.datastax.oss.driver.internal.core.util.concurrent.CapturingTimer; +import com.datastax.oss.driver.internal.core.util.concurrent.CapturingTimer.CapturedTimeout; +import com.datastax.oss.protocol.internal.Frame; +import io.netty.channel.EventLoopGroup; +import io.netty.util.TimerTask; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.OngoingStubbing; + +/** + * Provides the environment to test a request handler, where a query plan can be defined, and the + * behavior of each successive node simulated. + */ +public class RequestHandlerTestHarness implements AutoCloseable { + + public static Builder builder() { + return new Builder(); + } + + private final CapturingTimer timer = new CapturingTimer(); + private final Map pools; + + @Mock protected InternalDriverContext context; + @Mock protected DefaultSession session; + @Mock protected EventLoopGroup eventLoopGroup; + @Mock protected NettyOptions nettyOptions; + @Mock protected DriverConfig config; + @Mock protected DriverExecutionProfile defaultProfile; + @Mock protected LoadBalancingPolicyWrapper loadBalancingPolicyWrapper; + @Mock protected RetryPolicy retryPolicy; + @Mock protected SpeculativeExecutionPolicy speculativeExecutionPolicy; + @Mock protected TimestampGenerator timestampGenerator; + @Mock protected ProtocolVersionRegistry protocolVersionRegistry; + @Mock protected SessionMetricUpdater sessionMetricUpdater; + + protected RequestHandlerTestHarness(Builder builder) { + MockitoAnnotations.initMocks(this); + + when(nettyOptions.getTimer()).thenReturn(timer); + when(nettyOptions.ioEventLoopGroup()).thenReturn(eventLoopGroup); + when(context.getNettyOptions()).thenReturn(nettyOptions); + + when(defaultProfile.getName()).thenReturn(DriverExecutionProfile.DEFAULT_NAME); + // TODO make configurable in the test, also handle profiles + when(defaultProfile.getDuration(DefaultDriverOption.REQUEST_TIMEOUT)) + .thenReturn(Duration.ofMillis(500)); + when(defaultProfile.getString(DefaultDriverOption.REQUEST_CONSISTENCY)) + .thenReturn(DefaultConsistencyLevel.LOCAL_ONE.name()); + when(defaultProfile.getInt(DefaultDriverOption.REQUEST_PAGE_SIZE)).thenReturn(5000); + when(defaultProfile.getString(DefaultDriverOption.REQUEST_SERIAL_CONSISTENCY)) + .thenReturn(DefaultConsistencyLevel.SERIAL.name()); + when(defaultProfile.getBoolean(DefaultDriverOption.REQUEST_DEFAULT_IDEMPOTENCE)) + .thenReturn(builder.defaultIdempotence); + when(defaultProfile.getBoolean(DefaultDriverOption.PREPARE_ON_ALL_NODES)).thenReturn(true); + + when(config.getDefaultProfile()).thenReturn(defaultProfile); + when(context.getConfig()).thenReturn(config); + + when(loadBalancingPolicyWrapper.newQueryPlan( + any(Request.class), anyString(), any(Session.class))) + .thenReturn(builder.buildQueryPlan()); + when(context.getLoadBalancingPolicyWrapper()).thenReturn(loadBalancingPolicyWrapper); + + when(context.getRetryPolicy(anyString())).thenReturn(retryPolicy); + + // Disable speculative executions by default + when(speculativeExecutionPolicy.nextExecution( + any(Node.class), any(CqlIdentifier.class), any(Request.class), anyInt())) + .thenReturn(-1L); + when(context.getSpeculativeExecutionPolicy(anyString())).thenReturn(speculativeExecutionPolicy); + + when(context.getCodecRegistry()).thenReturn(new DefaultCodecRegistry("test")); + + when(timestampGenerator.next()).thenReturn(Long.MIN_VALUE); + when(context.getTimestampGenerator()).thenReturn(timestampGenerator); + + pools = builder.buildMockPools(); + when(session.getChannel(any(Node.class), anyString())) + .thenAnswer( + invocation -> { + Node node = invocation.getArgument(0); + return pools.get(node).next(); + }); + when(session.getRepreparePayloads()).thenReturn(new ConcurrentHashMap<>()); + + when(session.setKeyspace(any(CqlIdentifier.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + + when(session.getMetricUpdater()).thenReturn(sessionMetricUpdater); + when(sessionMetricUpdater.isEnabled(any(SessionMetric.class), anyString())).thenReturn(true); + + when(session.getMetadata()).thenReturn(DefaultMetadata.EMPTY); + + when(context.getProtocolVersionRegistry()).thenReturn(protocolVersionRegistry); + when(protocolVersionRegistry.supports(any(ProtocolVersion.class), any(ProtocolFeature.class))) + .thenReturn(true); + + if (builder.protocolVersion != null) { + when(context.getProtocolVersion()).thenReturn(builder.protocolVersion); + } + + when(context.getConsistencyLevelRegistry()).thenReturn(new DefaultConsistencyLevelRegistry()); + + when(context.getWriteTypeRegistry()).thenReturn(new DefaultWriteTypeRegistry()); + + when(context.getRequestThrottler()).thenReturn(new PassThroughRequestThrottler(context)); + + when(context.getRequestTracker()).thenReturn(new NoopRequestTracker(context)); + } + + public DefaultSession getSession() { + return session; + } + + public InternalDriverContext getContext() { + return context; + } + + public DriverChannel getChannel(Node node) { + ChannelPool pool = pools.get(node); + return pool.next(); + } + + /** + * Returns the next task that was scheduled on the request handler's admin executor. The test must + * run it manually. + */ + public CapturedTimeout nextScheduledTimeout() { + return timer.getNextTimeout(); + } + + public void runNextTask() { + TimerTask task = timer.getNextTimeout().task(); + } + + @Override + public void close() { + timer.stop(); + } + + public static class Builder { + private final List poolBehaviors = new ArrayList<>(); + private boolean defaultIdempotence; + private ProtocolVersion protocolVersion; + + /** + * Sets the given node as the next one in the query plan; an empty pool will be simulated when + * it gets used. + */ + public Builder withEmptyPool(Node node) { + poolBehaviors.add(new PoolBehavior(node, false)); + return this; + } + + /** + * Sets the given node as the next one in the query plan; a channel write failure will be + * simulated when it gets used. + */ + public Builder withWriteFailure(Node node, Throwable cause) { + PoolBehavior behavior = new PoolBehavior(node, true); + behavior.setWriteFailure(cause); + poolBehaviors.add(behavior); + return this; + } + + /** + * Sets the given node as the next one in the query plan; the write to the channel will succeed, + * but a response failure will be simulated immediately after. + */ + public Builder withResponseFailure(Node node, Throwable cause) { + PoolBehavior behavior = new PoolBehavior(node, true); + behavior.setWriteSuccess(); + behavior.setResponseFailure(cause); + poolBehaviors.add(behavior); + return this; + } + + /** + * Sets the given node as the next one in the query plan; the write to the channel will succeed, + * and the given response will be simulated immediately after. + */ + public Builder withResponse(Node node, Frame response) { + PoolBehavior behavior = new PoolBehavior(node, true); + behavior.setWriteSuccess(); + behavior.setResponseSuccess(response); + poolBehaviors.add(behavior); + return this; + } + + public Builder withDefaultIdempotence(boolean defaultIdempotence) { + this.defaultIdempotence = defaultIdempotence; + return this; + } + + public Builder withProtocolVersion(ProtocolVersion protocolVersion) { + this.protocolVersion = protocolVersion; + return this; + } + + /** + * Sets the given node as the next one in the query plan; the test code is responsible of + * calling the methods on the returned object to complete the write and the query. + */ + public PoolBehavior customBehavior(Node node) { + PoolBehavior behavior = new PoolBehavior(node, true); + poolBehaviors.add(behavior); + return behavior; + } + + public RequestHandlerTestHarness build() { + return new RequestHandlerTestHarness(this); + } + + private Queue buildQueryPlan() { + ConcurrentLinkedQueue queryPlan = new ConcurrentLinkedQueue<>(); + for (PoolBehavior behavior : poolBehaviors) { + // We don't want duplicates in the query plan: the only way a node is tried multiple times + // is if the retry policy returns a RETRY_SAME, the request handler does not re-read from + // the plan. + if (!queryPlan.contains(behavior.node)) { + queryPlan.offer(behavior.node); + } + } + return queryPlan; + } + + private Map buildMockPools() { + Map pools = new ConcurrentHashMap<>(); + Map> stubbings = new HashMap<>(); + for (PoolBehavior behavior : poolBehaviors) { + Node node = behavior.node; + ChannelPool pool = pools.computeIfAbsent(node, n -> mock(ChannelPool.class)); + + // The goal of the code below is to generate the equivalent of: + // + // when(pool.next()) + // .thenReturn(behavior1.channel) + // .thenReturn(behavior2.channel) + // ... + stubbings.compute( + node, + (sameNode, previous) -> { + if (previous == null) { + previous = when(pool.next()); + } + return previous.thenReturn(behavior.channel); + }); + } + return pools; + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/ResultSetTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/ResultSetTestBase.java new file mode 100644 index 00000000000..5ac3c8531d5 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/ResultSetTestBase.java @@ -0,0 +1,87 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.ExecutionInfo; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.internal.core.util.CountingIterator; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +public abstract class ResultSetTestBase { + + /** Mocks an async result set where column 0 has type INT, with rows with the provided data. */ + protected AsyncResultSet mockPage(boolean nextPage, Integer... data) { + AsyncResultSet page = mock(AsyncResultSet.class); + + ColumnDefinitions columnDefinitions = mock(ColumnDefinitions.class); + when(page.getColumnDefinitions()).thenReturn(columnDefinitions); + + ExecutionInfo executionInfo = mock(ExecutionInfo.class); + when(page.getExecutionInfo()).thenReturn(executionInfo); + + if (nextPage) { + when(page.hasMorePages()).thenReturn(true); + when(page.fetchNextPage()).thenReturn(spy(new CompletableFuture<>())); + } else { + when(page.hasMorePages()).thenReturn(false); + when(page.fetchNextPage()).thenThrow(new IllegalStateException()); + } + + // Emulate DefaultAsyncResultSet's internals (this is a bit sketchy, maybe it would be better + // to use real DefaultAsyncResultSet instances) + Queue queue = Lists.newLinkedList(Arrays.asList(data)); + CountingIterator iterator = + new CountingIterator(queue.size()) { + @Override + protected Row computeNext() { + Integer index = queue.poll(); + return (index == null) ? endOfData() : mockRow(index); + } + }; + when(page.currentPage()).thenReturn(() -> iterator); + when(page.remaining()).thenAnswer(invocation -> iterator.remaining()); + + return page; + } + + private Row mockRow(int index) { + Row row = mock(Row.class); + when(row.getInt(0)).thenReturn(index); + return row; + } + + protected static void complete(CompletionStage stage, AsyncResultSet result) { + stage.toCompletableFuture().complete(result); + } + + protected void assertNextRow(Iterator iterator, int expectedValue) { + assertThat(iterator.hasNext()).isTrue(); + Row row = iterator.next(); + assertThat(row.getInt(0)).isEqualTo(expectedValue); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/ResultSetsTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/ResultSetsTest.java new file mode 100644 index 00000000000..3d52fc3d22e --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/ResultSetsTest.java @@ -0,0 +1,97 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.cql.AsyncResultSet; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import java.util.Iterator; +import org.junit.Test; + +public class ResultSetsTest extends ResultSetTestBase { + + @Test + public void should_create_result_set_from_single_page() { + // Given + AsyncResultSet page1 = mockPage(false, 0, 1, 2); + + // When + ResultSet resultSet = ResultSets.newInstance(page1); + + // Then + assertThat(resultSet.getColumnDefinitions()).isSameAs(page1.getColumnDefinitions()); + assertThat(resultSet.getExecutionInfo()).isSameAs(page1.getExecutionInfo()); + assertThat(resultSet.getExecutionInfos()).containsExactly(page1.getExecutionInfo()); + + Iterator iterator = resultSet.iterator(); + + assertNextRow(iterator, 0); + assertNextRow(iterator, 1); + assertNextRow(iterator, 2); + + assertThat(iterator.hasNext()).isFalse(); + } + + @Test + public void should_create_result_set_from_multiple_pages() { + // Given + AsyncResultSet page1 = mockPage(true, 0, 1, 2); + AsyncResultSet page2 = mockPage(true, 3, 4, 5); + AsyncResultSet page3 = mockPage(false, 6, 7, 8); + + complete(page1.fetchNextPage(), page2); + complete(page2.fetchNextPage(), page3); + + // When + ResultSet resultSet = ResultSets.newInstance(page1); + + // Then + assertThat(resultSet.iterator().hasNext()).isTrue(); + + assertThat(resultSet.getColumnDefinitions()).isSameAs(page1.getColumnDefinitions()); + assertThat(resultSet.getExecutionInfo()).isSameAs(page1.getExecutionInfo()); + assertThat(resultSet.getExecutionInfos()).containsExactly(page1.getExecutionInfo()); + + Iterator iterator = resultSet.iterator(); + + assertNextRow(iterator, 0); + assertNextRow(iterator, 1); + assertNextRow(iterator, 2); + + assertThat(iterator.hasNext()).isTrue(); + // This should have triggered the fetch of page2 + assertThat(resultSet.getExecutionInfo()).isEqualTo(page2.getExecutionInfo()); + assertThat(resultSet.getExecutionInfos()) + .containsExactly(page1.getExecutionInfo(), page2.getExecutionInfo()); + + assertNextRow(iterator, 3); + assertNextRow(iterator, 4); + assertNextRow(iterator, 5); + + assertThat(iterator.hasNext()).isTrue(); + // This should have triggered the fetch of page3 + assertThat(resultSet.getExecutionInfo()).isEqualTo(page3.getExecutionInfo()); + assertThat(resultSet.getExecutionInfos()) + .containsExactly( + page1.getExecutionInfo(), page2.getExecutionInfo(), page3.getExecutionInfo()); + + assertNextRow(iterator, 6); + assertNextRow(iterator, 7); + assertNextRow(iterator, 8); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/cql/StatementSizeTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/StatementSizeTest.java new file mode 100644 index 00000000000..59ca780136f --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/cql/StatementSizeTest.java @@ -0,0 +1,289 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.cql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.ColumnDefinitions; +import com.datastax.oss.driver.api.core.cql.DefaultBatchType; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.time.TimestampGenerator; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.CassandraProtocolVersionRegistry; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.shaded.guava.common.base.Charsets; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.response.result.ColumnSpec; +import com.datastax.oss.protocol.internal.response.result.RawType; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class StatementSizeTest { + + private static final byte[] MOCK_PAGING_STATE = Bytes.getArray(Bytes.fromHexString("0xdeadbeef")); + private static final ByteBuffer MOCK_PAYLOAD_VALUE1 = Bytes.fromHexString("0xabcd"); + private static final ByteBuffer MOCK_PAYLOAD_VALUE2 = Bytes.fromHexString("0xef"); + private static final ImmutableMap MOCK_PAYLOAD = + ImmutableMap.of("key1", MOCK_PAYLOAD_VALUE1, "key2", MOCK_PAYLOAD_VALUE2); + private static final byte[] PREPARED_ID = Bytes.getArray(Bytes.fromHexString("0xaaaa")); + private static final byte[] RESULT_METADATA_ID = Bytes.getArray(Bytes.fromHexString("0xbbbb")); + + @Mock PreparedStatement preparedStatement; + @Mock InternalDriverContext driverContext; + @Mock DriverConfig config; + @Mock DriverExecutionProfile defaultProfile; + @Mock TimestampGenerator timestampGenerator; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + ByteBuffer preparedId = ByteBuffer.wrap(PREPARED_ID); + when(preparedStatement.getId()).thenReturn(preparedId); + ByteBuffer resultMetadataId = ByteBuffer.wrap(RESULT_METADATA_ID); + when(preparedStatement.getResultMetadataId()).thenReturn(resultMetadataId); + + ColumnDefinitions columnDefinitions = + DefaultColumnDefinitions.valueOf( + ImmutableList.of( + phonyColumnDef("ks", "table", "c1", -1, ProtocolConstants.DataType.INT), + phonyColumnDef("ks", "table", "c2", -1, ProtocolConstants.DataType.VARCHAR))); + + when(preparedStatement.getVariableDefinitions()).thenReturn(columnDefinitions); + + when(driverContext.getProtocolVersion()).thenReturn(DefaultProtocolVersion.V5); + when(driverContext.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT); + when(driverContext.getProtocolVersionRegistry()) + .thenReturn(new CassandraProtocolVersionRegistry(null)); + when(config.getDefaultProfile()).thenReturn(defaultProfile); + when(driverContext.getConfig()).thenReturn(config); + when(driverContext.getTimestampGenerator()).thenReturn(timestampGenerator); + } + + private ColumnDefinition phonyColumnDef( + String keyspace, String table, String column, int index, int typeCode) { + return new DefaultColumnDefinition( + new ColumnSpec(keyspace, table, column, index, RawType.PRIMITIVES.get(typeCode)), + AttachmentPoint.NONE); + } + + @Test + public void should_measure_size_of_simple_statement() { + String queryString = "SELECT release_version FROM system.local WHERE key = ?"; + SimpleStatement statement = SimpleStatement.newInstance(queryString); + int expectedSize = + 9 // header + + (4 + queryString.getBytes(Charsets.UTF_8).length) // query string + + 2 // consistency level + + 2 // serial consistency level + + 4 // fetch size + + 8 // timestamp + + 4; // flags + + assertThat(v5SizeOf(statement)).isEqualTo(expectedSize); + + String value1 = "local"; + SimpleStatement statementWithAnonymousValue = SimpleStatement.newInstance(queryString, value1); + assertThat(v5SizeOf(statementWithAnonymousValue)) + .isEqualTo( + expectedSize + + 2 // size of number of values + + (4 + value1.getBytes(Charsets.UTF_8).length) // value + ); + + String key1 = "key"; + SimpleStatement statementWithNamedValue = + SimpleStatement.newInstance(queryString, ImmutableMap.of(key1, value1)); + assertThat(v5SizeOf(statementWithNamedValue)) + .isEqualTo( + expectedSize + + 2 // size of number of values + + (2 + key1.getBytes(Charsets.UTF_8).length) // key + + (4 + value1.getBytes(Charsets.UTF_8).length) // value + ); + + SimpleStatement statementWithPagingState = + statement.setPagingState(ByteBuffer.wrap(MOCK_PAGING_STATE)); + assertThat(v5SizeOf(statementWithPagingState)) + .isEqualTo(expectedSize + 4 + MOCK_PAGING_STATE.length); + + SimpleStatement statementWithPayload = statement.setCustomPayload(MOCK_PAYLOAD); + assertThat(v5SizeOf(statementWithPayload)) + .isEqualTo( + expectedSize + + 2 // size of number of keys in the map + // size of each key/value pair + + (2 + "key1".getBytes(Charsets.UTF_8).length) + + (4 + MOCK_PAYLOAD_VALUE1.remaining()) + + (2 + "key2".getBytes(Charsets.UTF_8).length) + + (4 + MOCK_PAYLOAD_VALUE2.remaining())); + + SimpleStatement statementWithKeyspace = statement.setKeyspace("testKeyspace"); + assertThat(v5SizeOf(statementWithKeyspace)) + .isEqualTo(expectedSize + 2 + "testKeyspace".getBytes(Charsets.UTF_8).length); + } + + @Test + public void should_measure_size_of_bound_statement() { + + BoundStatement statement = + newBoundStatement( + preparedStatement, + new ByteBuffer[] {ProtocolConstants.UNSET_VALUE, ProtocolConstants.UNSET_VALUE}); + + int expectedSize = + 9 // header size + + 4 // flags + + 2 // consistency level + + 2 // serial consistency level + + 8 // timestamp + + (2 + PREPARED_ID.length) + + (2 + RESULT_METADATA_ID.length) + + 2 // size of value list + + 2 * (4) // two null values (size = -1) + + 4 // fetch size + ; + assertThat(v5SizeOf(statement)).isEqualTo(expectedSize); + + BoundStatement withValues = statement.setInt(0, 0).setString(1, "test"); + expectedSize += + 4 // the size of the int value + + "test".getBytes(Charsets.UTF_8).length; + assertThat(v5SizeOf(withValues)).isEqualTo(expectedSize); + + BoundStatement withPagingState = withValues.setPagingState(ByteBuffer.wrap(MOCK_PAGING_STATE)); + expectedSize += 4 + MOCK_PAGING_STATE.length; + assertThat(v5SizeOf(withPagingState)).isEqualTo(expectedSize); + + BoundStatement withPayload = withPagingState.setCustomPayload(MOCK_PAYLOAD); + expectedSize += + 2 // size of number of keys in the map + // size of each key/value pair + + (2 + "key1".getBytes(Charsets.UTF_8).length) + + (4 + MOCK_PAYLOAD_VALUE1.remaining()) + + (2 + "key2".getBytes(Charsets.UTF_8).length) + + (4 + MOCK_PAYLOAD_VALUE2.remaining()); + assertThat(v5SizeOf(withPayload)).isEqualTo(expectedSize); + } + + @Test + public void should_measure_size_of_batch_statement() { + String queryString = "SELECT release_version FROM system.local"; + String key1 = "key"; + String value1 = "value"; + SimpleStatement statement1 = + SimpleStatement.newInstance(queryString, ImmutableMap.of(key1, value1)); + + BoundStatement statement2 = + newBoundStatement( + preparedStatement, + new ByteBuffer[] {ProtocolConstants.UNSET_VALUE, ProtocolConstants.UNSET_VALUE}) + .setInt(0, 0) + .setString(1, "test"); + BoundStatement statement3 = + newBoundStatement( + preparedStatement, + new ByteBuffer[] {ProtocolConstants.UNSET_VALUE, ProtocolConstants.UNSET_VALUE}) + .setInt(0, 0) + .setString(1, "test2"); + + BatchStatement batchStatement = + BatchStatement.newInstance(DefaultBatchType.UNLOGGED) + .add(statement1) + .add(statement2) + .add(statement3); + + int expectedSize = + 9 // header size + + 1 + + 2 // batch type + number of queries + // statements' type of id + id (query string/prepared id): + + 1 + + (4 + queryString.getBytes(Charsets.UTF_8).length) + + 1 + + (2 + PREPARED_ID.length) + + 1 + + (2 + PREPARED_ID.length) + // simple statement values + + 2 // size of number of values + + (2 + key1.getBytes(Charsets.UTF_8).length) // key + + (4 + value1.getBytes(Charsets.UTF_8).length) // value + // bound statements values + + (2 + (4 + 4) + (4 + "test".getBytes(Charsets.UTF_8).length)) + + (2 + (4 + 4) + (4 + "test2".getBytes(Charsets.UTF_8).length)) + + 2 // consistency level + + 2 // serial consistency level + + 8 // timestamp + + 4; // flags + assertThat(v5SizeOf(batchStatement)).isEqualTo(expectedSize); + + BatchStatement withPayload = batchStatement.setCustomPayload(MOCK_PAYLOAD); + expectedSize += + 2 // size of number of keys in the map + // size of each key/value pair + + (2 + "key1".getBytes(Charsets.UTF_8).length) + + (4 + MOCK_PAYLOAD_VALUE1.remaining()) + + (2 + "key2".getBytes(Charsets.UTF_8).length) + + (4 + MOCK_PAYLOAD_VALUE2.remaining()); + assertThat(v5SizeOf(withPayload)).isEqualTo(expectedSize); + } + + private int v5SizeOf(Statement statement) { + return statement.computeSizeInBytes(driverContext); + } + + private BoundStatement newBoundStatement( + PreparedStatement preparedStatement, ByteBuffer[] initialValues) { + return new DefaultBoundStatement( + preparedStatement, + preparedStatement.getVariableDefinitions(), + initialValues, + null, + null, + null, + null, + null, + Collections.emptyMap(), + null, + false, + -1, + null, + Integer.MIN_VALUE, + null, + null, + null, + CodecRegistry.DEFAULT, + DefaultProtocolVersion.V5, + null); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/data/AccessibleByIdTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/data/AccessibleByIdTestBase.java new file mode 100644 index 00000000000..ad3ee2f199e --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/data/AccessibleByIdTestBase.java @@ -0,0 +1,483 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.data; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.GettableById; +import com.datastax.oss.driver.api.core.data.GettableByName; +import com.datastax.oss.driver.api.core.data.SettableById; +import com.datastax.oss.driver.api.core.data.SettableByName; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.type.codec.CqlIntToStringCodec; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import org.junit.Test; + +public abstract class AccessibleByIdTestBase< + T extends GettableById & SettableById & GettableByName & SettableByName> + extends AccessibleByIndexTestBase { + + private static final CqlIdentifier FIELD0_ID = CqlIdentifier.fromInternal("field0"); + private static final String FIELD0_NAME = "field0"; + + @Test + public void should_set_primitive_value_by_id() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.setInt(FIELD0_ID, 1); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, Integer.class); + verify(intCodec).encodePrimitive(1, ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(FIELD0_ID)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_object_value_by_id() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.TEXT), attachmentPoint); + + // When + t = t.setString(FIELD0_ID, "a"); + + // Then + verify(codecRegistry).codecFor(DataTypes.TEXT, String.class); + verify(textCodec).encode("a", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(FIELD0_ID)).isEqualTo(Bytes.fromHexString("0x61")); + } + + @Test + public void should_set_bytes_by_id() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.setBytesUnsafe(FIELD0_ID, Bytes.fromHexString("0x00000001")); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(t.getBytesUnsafe(FIELD0_ID)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_to_null_by_id() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_ID, Bytes.fromHexString("0x00000001")); + + // When + t = t.setToNull(FIELD0_ID); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(t.getBytesUnsafe(FIELD0_ID)).isNull(); + } + + @Test + public void should_set_with_explicit_class_by_id() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, String.class)).thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.set(FIELD0_ID, "1", String.class); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, String.class); + verify(intToStringCodec).encode("1", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(FIELD0_ID)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_with_explicit_type_by_id() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, GenericType.STRING)) + .thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.set(FIELD0_ID, "1", GenericType.STRING); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, GenericType.STRING); + verify(intToStringCodec).encode("1", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(FIELD0_ID)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_with_explicit_codec_by_id() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.set(FIELD0_ID, "1", intToStringCodec); + + // Then + verifyZeroInteractions(codecRegistry); + verify(intToStringCodec).encode("1", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(FIELD0_ID)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_get_primitive_value_by_id() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_ID, Bytes.fromHexString("0x00000001")); + + // When + int i = t.getInt(FIELD0_ID); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, Integer.class); + verify(intCodec).decodePrimitive(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(i).isEqualTo(1); + } + + @Test + public void should_get_object_value_by_id() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.TEXT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_ID, Bytes.fromHexString("0x61")); + + // When + String s = t.getString(FIELD0_ID); + + // Then + verify(codecRegistry).codecFor(DataTypes.TEXT, String.class); + verify(textCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("a"); + } + + @Test + public void should_get_bytes_by_id() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_ID, Bytes.fromHexString("0x00000001")); + + // When + ByteBuffer bytes = t.getBytesUnsafe(FIELD0_ID); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(bytes).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_test_if_null_by_id() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_ID, null); + + // When + boolean isNull = t.isNull(FIELD0_ID); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(isNull).isTrue(); + } + + @Test + public void should_get_with_explicit_class_by_id() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, String.class)).thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_ID, Bytes.fromHexString("0x00000001")); + + // When + String s = t.get(FIELD0_ID, String.class); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, String.class); + verify(intToStringCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("1"); + } + + @Test + public void should_get_with_explicit_type_by_id() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, GenericType.STRING)) + .thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_ID, Bytes.fromHexString("0x00000001")); + + // When + String s = t.get(FIELD0_ID, GenericType.STRING); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, GenericType.STRING); + verify(intToStringCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("1"); + } + + @Test + public void should_get_with_explicit_codec_by_id() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_ID, Bytes.fromHexString("0x00000001")); + + // When + String s = t.get(FIELD0_ID, intToStringCodec); + + // Then + verifyZeroInteractions(codecRegistry); + verify(intToStringCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("1"); + } + + @Test + public void should_set_primitive_value_by_name() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.setInt(FIELD0_NAME, 1); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, Integer.class); + verify(intCodec).encodePrimitive(1, ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(FIELD0_NAME)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_object_value_by_name() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.TEXT), attachmentPoint); + + // When + t = t.setString(FIELD0_NAME, "a"); + + // Then + verify(codecRegistry).codecFor(DataTypes.TEXT, String.class); + verify(textCodec).encode("a", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(FIELD0_NAME)).isEqualTo(Bytes.fromHexString("0x61")); + } + + @Test + public void should_set_bytes_by_name() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.setBytesUnsafe(FIELD0_NAME, Bytes.fromHexString("0x00000001")); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(t.getBytesUnsafe(FIELD0_NAME)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_to_null_by_name() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_NAME, Bytes.fromHexString("0x00000001")); + + // When + t = t.setToNull(FIELD0_NAME); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(t.getBytesUnsafe(FIELD0_NAME)).isNull(); + } + + @Test + public void should_set_with_explicit_class_by_name() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, String.class)).thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.set(FIELD0_NAME, "1", String.class); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, String.class); + verify(intToStringCodec).encode("1", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(FIELD0_NAME)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_with_explicit_type_by_name() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, GenericType.STRING)) + .thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.set(FIELD0_NAME, "1", GenericType.STRING); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, GenericType.STRING); + verify(intToStringCodec).encode("1", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(FIELD0_NAME)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_with_explicit_codec_by_name() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.set(FIELD0_NAME, "1", intToStringCodec); + + // Then + verifyZeroInteractions(codecRegistry); + verify(intToStringCodec).encode("1", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(FIELD0_NAME)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_get_primitive_value_by_name() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_NAME, Bytes.fromHexString("0x00000001")); + + // When + int i = t.getInt(FIELD0_NAME); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, Integer.class); + verify(intCodec).decodePrimitive(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(i).isEqualTo(1); + } + + @Test + public void should_get_object_value_by_name() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.TEXT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_NAME, Bytes.fromHexString("0x61")); + + // When + String s = t.getString(FIELD0_NAME); + + // Then + verify(codecRegistry).codecFor(DataTypes.TEXT, String.class); + verify(textCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("a"); + } + + @Test + public void should_get_bytes_by_name() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_NAME, Bytes.fromHexString("0x00000001")); + + // When + ByteBuffer bytes = t.getBytesUnsafe(FIELD0_NAME); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(bytes).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_test_if_null_by_name() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_NAME, null); + + // When + boolean isNull = t.isNull(FIELD0_NAME); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(isNull).isTrue(); + } + + @Test + public void should_get_with_explicit_class_by_name() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, String.class)).thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_NAME, Bytes.fromHexString("0x00000001")); + + // When + String s = t.get(FIELD0_NAME, String.class); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, String.class); + verify(intToStringCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("1"); + } + + @Test + public void should_get_with_explicit_type_by_name() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, GenericType.STRING)) + .thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_NAME, Bytes.fromHexString("0x00000001")); + + // When + String s = t.get(FIELD0_NAME, GenericType.STRING); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, GenericType.STRING); + verify(intToStringCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("1"); + } + + @Test + public void should_get_with_explicit_codec_by_name() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(FIELD0_NAME, Bytes.fromHexString("0x00000001")); + + // When + String s = t.get(FIELD0_NAME, intToStringCodec); + + // Then + verifyZeroInteractions(codecRegistry); + verify(intToStringCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("1"); + } + + @SuppressWarnings("UnusedAssignment") + @Test(expected = IllegalArgumentException.class) + public void should_fail_when_id_does_not_exists() { + final CqlIdentifier invalidField = CqlIdentifier.fromInternal("invalidField"); + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.setInt(invalidField, 1); + + // Then the method will throw IllegalArgumentException up to the client. + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/data/AccessibleByIndexTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/data/AccessibleByIndexTestBase.java new file mode 100644 index 00000000000..3239a655ece --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/data/AccessibleByIndexTestBase.java @@ -0,0 +1,342 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.data; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.GettableByIndex; +import com.datastax.oss.driver.api.core.data.SettableByIndex; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveIntCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.type.codec.CqlIntToStringCodec; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public abstract class AccessibleByIndexTestBase> { + + protected abstract T newInstance(List dataTypes, AttachmentPoint attachmentPoint); + + protected abstract T newInstance( + List dataTypes, List values, AttachmentPoint attachmentPoint); + + @Mock protected AttachmentPoint attachmentPoint; + @Mock protected AttachmentPoint v3AttachmentPoint; + @Mock protected CodecRegistry codecRegistry; + protected PrimitiveIntCodec intCodec; + protected TypeCodec doubleCodec; + protected TypeCodec textCodec; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(attachmentPoint.getCodecRegistry()).thenReturn(codecRegistry); + when(attachmentPoint.getProtocolVersion()).thenReturn(ProtocolVersion.DEFAULT); + + when(v3AttachmentPoint.getCodecRegistry()).thenReturn(codecRegistry); + when(v3AttachmentPoint.getProtocolVersion()).thenReturn(DefaultProtocolVersion.V3); + + intCodec = spy(TypeCodecs.INT); + doubleCodec = spy(TypeCodecs.DOUBLE); + textCodec = spy(TypeCodecs.TEXT); + + when(codecRegistry.codecFor(DataTypes.INT, Integer.class)).thenAnswer(i -> intCodec); + when(codecRegistry.codecFor(DataTypes.DOUBLE, Double.class)).thenAnswer(i -> doubleCodec); + when(codecRegistry.codecFor(DataTypes.TEXT, String.class)).thenAnswer(i -> textCodec); + + when(codecRegistry.codecFor(DataTypes.INT)).thenAnswer(i -> intCodec); + when(codecRegistry.codecFor(DataTypes.TEXT)).thenAnswer(t -> textCodec); + when(codecRegistry.codecFor(DataTypes.DOUBLE)).thenAnswer(d -> doubleCodec); + } + + @Test + public void should_set_primitive_value_by_index() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.setInt(0, 1); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, Integer.class); + verify(intCodec).encodePrimitive(1, ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(0)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_object_value_by_index() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.TEXT), attachmentPoint); + + // When + t = t.setString(0, "a"); + + // Then + verify(codecRegistry).codecFor(DataTypes.TEXT, String.class); + verify(textCodec).encode("a", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(0)).isEqualTo(Bytes.fromHexString("0x61")); + } + + @Test + public void should_set_bytes_by_index() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(t.getBytesUnsafe(0)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_to_null_by_index() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + + // When + t = t.setToNull(0); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(t.getBytesUnsafe(0)).isNull(); + } + + @Test + public void should_set_with_explicit_class_by_index() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, String.class)).thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.set(0, "1", String.class); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, String.class); + verify(intToStringCodec).encode("1", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(0)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_with_explicit_type_by_index() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, GenericType.STRING)) + .thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.set(0, "1", GenericType.STRING); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, GenericType.STRING); + verify(intToStringCodec).encode("1", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(0)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_with_explicit_codec_by_index() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + + // When + t = t.set(0, "1", intToStringCodec); + + // Then + verifyZeroInteractions(codecRegistry); + verify(intToStringCodec).encode("1", ProtocolVersion.DEFAULT); + assertThat(t.getBytesUnsafe(0)).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_set_values_in_bulk() { + // Given + when(codecRegistry.codecFor(DataTypes.TEXT, "foo")).thenReturn(TypeCodecs.TEXT); + when(codecRegistry.codecFor(DataTypes.INT, 1)).thenReturn(TypeCodecs.INT); + + // When + T t = + newInstance( + ImmutableList.of(DataTypes.TEXT, DataTypes.INT), + ImmutableList.of("foo", 1), + attachmentPoint); + + // Then + assertThat(t.getString(0)).isEqualTo("foo"); + assertThat(t.getInt(1)).isEqualTo(1); + verify(codecRegistry).codecFor(DataTypes.TEXT, "foo"); + verify(codecRegistry).codecFor(DataTypes.INT, 1); + } + + @Test + public void should_set_values_in_bulk_when_not_enough_values() { + // Given + when(codecRegistry.codecFor(DataTypes.TEXT, "foo")).thenReturn(TypeCodecs.TEXT); + + // When + T t = + newInstance( + ImmutableList.of(DataTypes.TEXT, DataTypes.INT), + ImmutableList.of("foo"), + attachmentPoint); + + // Then + assertThat(t.getString(0)).isEqualTo("foo"); + assertThat(t.isNull(1)).isTrue(); + verify(codecRegistry).codecFor(DataTypes.TEXT, "foo"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_set_values_in_bulk_when_too_many_values() { + newInstance( + ImmutableList.of(DataTypes.TEXT, DataTypes.INT), + ImmutableList.of("foo", 1, "bar"), + attachmentPoint); + } + + @Test + public void should_get_primitive_value_by_index() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + + // When + int i = t.getInt(0); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, Integer.class); + verify(intCodec).decodePrimitive(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(i).isEqualTo(1); + } + + @Test + public void should_get_object_value_by_index() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.TEXT), attachmentPoint); + t = t.setBytesUnsafe(0, Bytes.fromHexString("0x61")); + + // When + String s = t.getString(0); + + // Then + verify(codecRegistry).codecFor(DataTypes.TEXT, String.class); + verify(textCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("a"); + } + + @Test + public void should_get_bytes_by_index() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + + // When + ByteBuffer bytes = t.getBytesUnsafe(0); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(bytes).isEqualTo(Bytes.fromHexString("0x00000001")); + } + + @Test + public void should_test_if_null_by_index() { + // Given + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(0, null); + + // When + boolean isNull = t.isNull(0); + + // Then + verifyZeroInteractions(codecRegistry); + assertThat(isNull).isTrue(); + } + + @Test + public void should_get_with_explicit_class_by_index() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, String.class)).thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + + // When + String s = t.get(0, String.class); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, String.class); + verify(intToStringCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("1"); + } + + @Test + public void should_get_with_explicit_type_by_index() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + when(codecRegistry.codecFor(DataTypes.INT, GenericType.STRING)) + .thenAnswer(i -> intToStringCodec); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + + // When + String s = t.get(0, GenericType.STRING); + + // Then + verify(codecRegistry).codecFor(DataTypes.INT, GenericType.STRING); + verify(intToStringCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("1"); + } + + @Test + public void should_get_with_explicit_codec_by_index() { + // Given + CqlIntToStringCodec intToStringCodec = spy(new CqlIntToStringCodec()); + T t = newInstance(ImmutableList.of(DataTypes.INT), attachmentPoint); + t = t.setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + + // When + String s = t.get(0, intToStringCodec); + + // Then + verifyZeroInteractions(codecRegistry); + verify(intToStringCodec).decode(any(ByteBuffer.class), eq(ProtocolVersion.DEFAULT)); + assertThat(s).isEqualTo("1"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/data/DefaultTupleValueTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/data/DefaultTupleValueTest.java new file mode 100644 index 00000000000..07c1dc42a89 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/data/DefaultTupleValueTest.java @@ -0,0 +1,113 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.data; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.internal.SerializationHelper; +import com.datastax.oss.driver.internal.core.type.DefaultTupleType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.util.List; +import org.junit.Test; + +public class DefaultTupleValueTest extends AccessibleByIndexTestBase { + + @Override + protected TupleValue newInstance(List dataTypes, AttachmentPoint attachmentPoint) { + DefaultTupleType type = new DefaultTupleType(dataTypes, attachmentPoint); + return type.newValue(); + } + + @Override + protected TupleValue newInstance( + List dataTypes, List values, AttachmentPoint attachmentPoint) { + DefaultTupleType type = new DefaultTupleType(dataTypes, attachmentPoint); + return type.newValue(values.toArray()); + } + + @Test + public void should_serialize_and_deserialize() { + DefaultTupleType type = + new DefaultTupleType(ImmutableList.of(DataTypes.INT, DataTypes.TEXT), attachmentPoint); + TupleValue in = type.newValue(); + in = in.setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + in = in.setBytesUnsafe(1, Bytes.fromHexString("0x61")); + + TupleValue out = SerializationHelper.serializeAndDeserialize(in); + + assertThat(out.getType()).isEqualTo(in.getType()); + assertThat(out.getType().isDetached()).isTrue(); + assertThat(Bytes.toHexString(out.getBytesUnsafe(0))).isEqualTo("0x00000001"); + assertThat(Bytes.toHexString(out.getBytesUnsafe(1))).isEqualTo("0x61"); + } + + @Test + public void should_support_null_items_when_setting_in_bulk() { + DefaultTupleType type = + new DefaultTupleType(ImmutableList.of(DataTypes.INT, DataTypes.TEXT), attachmentPoint); + when(codecRegistry.codecFor(DataTypes.INT)).thenReturn(TypeCodecs.INT); + when(codecRegistry.codecFor(DataTypes.TEXT, "foo")).thenReturn(TypeCodecs.TEXT); + TupleValue value = type.newValue(null, "foo"); + + assertThat(value.isNull(0)).isTrue(); + assertThat(value.getString(1)).isEqualTo("foo"); + } + + @Test + public void should_equate_instances_with_same_values_but_different_binary_representations() { + TupleType tupleType = DataTypes.tupleOf(DataTypes.VARINT); + + TupleValue tuple1 = tupleType.newValue().setBytesUnsafe(0, Bytes.fromHexString("0x01")); + TupleValue tuple2 = tupleType.newValue().setBytesUnsafe(0, Bytes.fromHexString("0x0001")); + + assertThat(tuple1).isEqualTo(tuple2); + assertThat(tuple1.hashCode()).isEqualTo(tuple2.hashCode()); + } + + @Test + public void should_not_equate_instances_with_same_binary_representation_but_different_types() { + TupleType tupleType1 = DataTypes.tupleOf(DataTypes.INT); + TupleType tupleType2 = DataTypes.tupleOf(DataTypes.VARINT); + + TupleValue tuple1 = tupleType1.newValue().setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + TupleValue tuple2 = tupleType2.newValue().setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + + assertThat(tuple1).isNotEqualTo(tuple2); + } + + @Test + public void should_equate_instances_with_different_protocol_versions() { + TupleType tupleType1 = DataTypes.tupleOf(DataTypes.TEXT); + tupleType1.attach(attachmentPoint); + + // use the V3 attachmentPoint for type2 + TupleType tupleType2 = DataTypes.tupleOf(DataTypes.TEXT); + tupleType2.attach(v3AttachmentPoint); + + TupleValue tuple1 = tupleType1.newValue().setBytesUnsafe(0, Bytes.fromHexString("0x01")); + TupleValue tuple2 = tupleType2.newValue().setBytesUnsafe(0, Bytes.fromHexString("0x01")); + + assertThat(tuple1).isEqualTo(tuple2); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/data/DefaultUdtValueTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/data/DefaultUdtValueTest.java new file mode 100644 index 00000000000..c097528e46d --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/data/DefaultUdtValueTest.java @@ -0,0 +1,156 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.data; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.internal.SerializationHelper; +import com.datastax.oss.driver.internal.core.type.UserDefinedTypeBuilder; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.io.UnsupportedEncodingException; +import java.util.List; +import org.junit.Test; + +public class DefaultUdtValueTest extends AccessibleByIdTestBase { + + @Override + protected UdtValue newInstance(List dataTypes, AttachmentPoint attachmentPoint) { + UserDefinedTypeBuilder builder = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")); + for (int i = 0; i < dataTypes.size(); i++) { + builder.withField(CqlIdentifier.fromInternal("field" + i), dataTypes.get(i)); + } + UserDefinedType userDefinedType = builder.build(); + userDefinedType.attach(attachmentPoint); + return userDefinedType.newValue(); + } + + @Override + protected UdtValue newInstance( + List dataTypes, List values, AttachmentPoint attachmentPoint) { + UserDefinedTypeBuilder builder = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")); + for (int i = 0; i < dataTypes.size(); i++) { + builder.withField(CqlIdentifier.fromInternal("field" + i), dataTypes.get(i)); + } + UserDefinedType userDefinedType = builder.build(); + userDefinedType.attach(attachmentPoint); + return userDefinedType.newValue(values.toArray()); + } + + @Test + public void should_serialize_and_deserialize() { + UserDefinedType type = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("field1"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("field2"), DataTypes.TEXT) + .build(); + UdtValue in = type.newValue(); + in = in.setBytesUnsafe(0, Bytes.fromHexString("0x00000001")); + in = in.setBytesUnsafe(1, Bytes.fromHexString("0x61")); + + UdtValue out = SerializationHelper.serializeAndDeserialize(in); + + assertThat(out.getType()).isEqualTo(in.getType()); + assertThat(out.getType().isDetached()).isTrue(); + assertThat(Bytes.toHexString(out.getBytesUnsafe(0))).isEqualTo("0x00000001"); + assertThat(Bytes.toHexString(out.getBytesUnsafe(1))).isEqualTo("0x61"); + } + + @Test + public void should_support_null_items_when_setting_in_bulk() throws UnsupportedEncodingException { + UserDefinedType type = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("field1"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("field2"), DataTypes.TEXT) + .build(); + when(codecRegistry.codecFor(DataTypes.INT)).thenReturn(TypeCodecs.INT); + when(codecRegistry.codecFor(DataTypes.TEXT, "foo")).thenReturn(TypeCodecs.TEXT); + UdtValue value = type.newValue(null, "foo"); + + assertThat(value.isNull(0)).isTrue(); + assertThat(value.getString(1)).isEqualTo("foo"); + } + + @Test + public void should_equate_instances_with_same_values_but_different_binary_representations() { + UserDefinedType type = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("f"), DataTypes.VARINT) + .build(); + + UdtValue udt1 = type.newValue().setBytesUnsafe(0, Bytes.fromHexString("0x01")); + UdtValue udt2 = type.newValue().setBytesUnsafe(0, Bytes.fromHexString("0x0001")); + + assertThat(udt1).isEqualTo(udt2); + } + + @Test + public void should_format_to_string() { + UserDefinedType type = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("t"), DataTypes.TEXT) + .withField(CqlIdentifier.fromInternal("i"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("d"), DataTypes.DOUBLE) + .build(); + + UdtValue udt = type.newValue().setString("t", "foobar").setDouble("d", 3.14); + + assertThat(udt.toString()).isEqualTo("{t:'foobar',i:NULL,d:3.14}"); + } + + @Test + public void should_equate_instances_with_different_protocol_versions() { + + UserDefinedType type1 = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("t"), DataTypes.TEXT) + .withField(CqlIdentifier.fromInternal("i"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("d"), DataTypes.DOUBLE) + .build(); + type1.attach(attachmentPoint); + + // create an idential type, but with a different attachment point + UserDefinedType type2 = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("t"), DataTypes.TEXT) + .withField(CqlIdentifier.fromInternal("i"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("d"), DataTypes.DOUBLE) + .build(); + type2.attach(v3AttachmentPoint); + UdtValue udt1 = + type1.newValue().setString("t", "some text string").setInt("i", 42).setDouble("d", 3.14); + UdtValue udt2 = + type2.newValue().setString("t", "some text string").setInt("i", 42).setDouble("d", 3.14); + assertThat(udt1).isEqualTo(udt2); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/data/IdentifierIndexTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/data/IdentifierIndexTest.java new file mode 100644 index 00000000000..504b5a17740 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/data/IdentifierIndexTest.java @@ -0,0 +1,63 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import org.junit.Test; + +public class IdentifierIndexTest { + private static final CqlIdentifier Foo = CqlIdentifier.fromInternal("Foo"); + private static final CqlIdentifier foo = CqlIdentifier.fromInternal("foo"); + private static final CqlIdentifier fOO = CqlIdentifier.fromInternal("fOO"); + private IdentifierIndex index = new IdentifierIndex(ImmutableList.of(Foo, foo, fOO)); + + @Test + public void should_find_first_index_of_existing_identifier() { + assertThat(index.firstIndexOf(Foo)).isEqualTo(0); + assertThat(index.firstIndexOf(foo)).isEqualTo(1); + assertThat(index.firstIndexOf(fOO)).isEqualTo(2); + } + + @Test + public void should_not_find_index_of_nonexistent_identifier() { + assertThat(index.firstIndexOf(CqlIdentifier.fromInternal("FOO"))).isEqualTo(-1); + } + + @Test + public void should_find_first_index_of_case_insensitive_name() { + assertThat(index.firstIndexOf("foo")).isEqualTo(0); + } + + @Test + public void should_not_find_first_index_of_nonexistent_case_insensitive_name() { + assertThat(index.firstIndexOf("bar")).isEqualTo(-1); + } + + @Test + public void should_find_first_index_of_case_sensitive_name() { + assertThat(index.firstIndexOf("\"Foo\"")).isEqualTo(0); + assertThat(index.firstIndexOf("\"foo\"")).isEqualTo(1); + assertThat(index.firstIndexOf("\"fOO\"")).isEqualTo(2); + } + + @Test + public void should_not_find_index_of_nonexistent_case_sensitive_name() { + assertThat(index.firstIndexOf("\"FOO\"")).isEqualTo(-1); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyEventsTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyEventsTest.java new file mode 100644 index 00000000000..a1bec905103 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyEventsTest.java @@ -0,0 +1,163 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.loadbalancing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import java.util.UUID; +import java.util.function.Predicate; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultLoadBalancingPolicyEventsTest extends DefaultLoadBalancingPolicyTestBase { + + @Mock private Predicate filter; + + private DefaultLoadBalancingPolicy policy; + + @Before + @Override + public void setup() { + super.setup(); + + when(filter.test(any(Node.class))).thenReturn(true); + when(context.getNodeFilter(DriverExecutionProfile.DEFAULT_NAME)).thenReturn(filter); + + when(metadataManager.getContactPoints()).thenReturn(ImmutableSet.of(node1)); + + policy = new DefaultLoadBalancingPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + policy.init( + ImmutableMap.of(UUID.randomUUID(), node1, UUID.randomUUID(), node2), distanceReporter); + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1, node2); + + reset(distanceReporter); + } + + @Test + public void should_remove_down_node_from_live_set() { + // When + policy.onDown(node2); + + // Then + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1); + verify(distanceReporter, never()).setDistance(eq(node2), any(NodeDistance.class)); + // should have been called only once, during initialization, but not during onDown + verify(filter).test(node2); + } + + @Test + public void should_remove_removed_node_from_live_set() { + // When + policy.onRemove(node2); + + // Then + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1); + verify(distanceReporter, never()).setDistance(eq(node2), any(NodeDistance.class)); + // should have been called only once, during initialization, but not during onRemove + verify(filter).test(node2); + } + + @Test + public void should_set_added_node_to_local() { + // When + policy.onAdd(node3); + + // Then + verify(distanceReporter).setDistance(node3, NodeDistance.LOCAL); + verify(filter).test(node3); + // Not added to the live set yet, we're waiting for the pool to open + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1, node2); + } + + @Test + public void should_ignore_added_node_when_filtered() { + // Given + when(filter.test(node3)).thenReturn(false); + + // When + policy.onAdd(node3); + + // Then + verify(distanceReporter).setDistance(node3, NodeDistance.IGNORED); + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1, node2); + } + + @Test + public void should_ignore_added_node_when_remote_dc() { + // Given + when(node3.getDatacenter()).thenReturn("dc2"); + + // When + policy.onAdd(node3); + + // Then + verify(distanceReporter).setDistance(node3, NodeDistance.IGNORED); + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1, node2); + } + + @Test + public void should_add_up_node_to_live_set() { + // When + policy.onUp(node3); + + // Then + verify(distanceReporter).setDistance(node3, NodeDistance.LOCAL); + verify(filter).test(node3); + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1, node2, node3); + } + + @Test + public void should_ignore_up_node_when_filtered() { + // Given + when(filter.test(node3)).thenReturn(false); + + // When + policy.onUp(node3); + + // Then + verify(distanceReporter).setDistance(node3, NodeDistance.IGNORED); + verify(filter).test(node3); + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1, node2); + } + + @Test + public void should_ignore_up_node_when_remote_dc() { + // Given + when(node3.getDatacenter()).thenReturn("dc2"); + + // When + policy.onUp(node3); + + // Then + verify(distanceReporter).setDistance(node3, NodeDistance.IGNORED); + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1, node2); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyInitTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyInitTest.java new file mode 100644 index 00000000000..f1d2c68fa43 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyInitTest.java @@ -0,0 +1,198 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.loadbalancing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.filter; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import java.util.UUID; +import org.junit.Test; + +public class DefaultLoadBalancingPolicyInitTest extends DefaultLoadBalancingPolicyTestBase { + + @Test + public void should_use_local_dc_if_provided_via_config() { + // Given + // the parent class sets the config option to "dc1" + + // When + DefaultLoadBalancingPolicy policy = + new DefaultLoadBalancingPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + + // Then + assertThat(policy.localDc).isEqualTo("dc1"); + } + + @Test + public void should_use_local_dc_if_provided_via_context() { + // Given + when(context.getLocalDatacenter(DriverExecutionProfile.DEFAULT_NAME)).thenReturn("dc1"); + // note: programmatic takes priority, the config won't even be inspected so no need to stub the + // option to null + + // When + DefaultLoadBalancingPolicy policy = + new DefaultLoadBalancingPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + + // Then + assertThat(policy.localDc).isEqualTo("dc1"); + verify(defaultProfile, never()) + .getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, null); + } + + @Test + public void should_infer_local_dc_if_no_explicit_contact_points() { + // Given + when(defaultProfile.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, null)) + .thenReturn(null); + when(metadataManager.getContactPoints()).thenReturn(ImmutableSet.of(node1)); + when(metadataManager.wasImplicitContactPoint()).thenReturn(true); + DefaultLoadBalancingPolicy policy = + new DefaultLoadBalancingPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + + // When + policy.init(ImmutableMap.of(UUID.randomUUID(), node1), distanceReporter); + + // Then + assertThat(policy.localDc).isEqualTo("dc1"); + } + + @Test + public void should_require_local_dc_if_explicit_contact_points() { + // Given + when(defaultProfile.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, null)) + .thenReturn(null); + when(metadataManager.getContactPoints()).thenReturn(ImmutableSet.of(node2)); + when(metadataManager.wasImplicitContactPoint()).thenReturn(false); + DefaultLoadBalancingPolicy policy = + new DefaultLoadBalancingPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("You provided explicit contact points, the local DC must be specified"); + + // When + policy.init(ImmutableMap.of(UUID.randomUUID(), node2), distanceReporter); + } + + @Test + public void should_warn_if_contact_points_not_in_local_dc() { + // Given + when(node2.getDatacenter()).thenReturn("dc2"); + when(node3.getDatacenter()).thenReturn("dc3"); + when(metadataManager.getContactPoints()).thenReturn(ImmutableSet.of(node1, node2, node3)); + DefaultLoadBalancingPolicy policy = + new DefaultLoadBalancingPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + + // When + policy.init( + ImmutableMap.of( + UUID.randomUUID(), node1, UUID.randomUUID(), node2, UUID.randomUUID(), node3), + distanceReporter); + + // Then + verify(appender, atLeast(1)).doAppend(loggingEventCaptor.capture()); + Iterable warnLogs = + filter(loggingEventCaptor.getAllValues()).with("level", Level.WARN).get(); + assertThat(warnLogs).hasSize(1); + assertThat(warnLogs.iterator().next().getFormattedMessage()) + .contains( + "You specified dc1 as the local DC, but some contact points are from a different DC") + .contains("node2=dc2") + .contains("node3=dc3"); + } + + @Test + public void should_include_nodes_from_local_dc() { + // Given + when(metadataManager.getContactPoints()).thenReturn(ImmutableSet.of(node1, node2)); + when(node1.getState()).thenReturn(NodeState.UP); + when(node2.getState()).thenReturn(NodeState.DOWN); + when(node3.getState()).thenReturn(NodeState.UNKNOWN); + DefaultLoadBalancingPolicy policy = + new DefaultLoadBalancingPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + + // When + policy.init( + ImmutableMap.of( + UUID.randomUUID(), node1, UUID.randomUUID(), node2, UUID.randomUUID(), node3), + distanceReporter); + + // Then + // Set distance for all nodes in the local DC + verify(distanceReporter).setDistance(node1, NodeDistance.LOCAL); + verify(distanceReporter).setDistance(node2, NodeDistance.LOCAL); + verify(distanceReporter).setDistance(node3, NodeDistance.LOCAL); + // But only include UP or UNKNOWN nodes in the live set + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1, node3); + } + + @Test + public void should_ignore_nodes_from_remote_dcs() { + // Given + when(node2.getDatacenter()).thenReturn("dc2"); + when(node3.getDatacenter()).thenReturn("dc3"); + when(metadataManager.getContactPoints()).thenReturn(ImmutableSet.of(node1, node2)); + DefaultLoadBalancingPolicy policy = + new DefaultLoadBalancingPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + + // When + policy.init( + ImmutableMap.of( + UUID.randomUUID(), node1, UUID.randomUUID(), node2, UUID.randomUUID(), node3), + distanceReporter); + + // Then + verify(distanceReporter).setDistance(node1, NodeDistance.LOCAL); + verify(distanceReporter).setDistance(node2, NodeDistance.IGNORED); + verify(distanceReporter).setDistance(node3, NodeDistance.IGNORED); + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1); + } + + @Test + public void should_ignore_nodes_excluded_by_filter() { + // Given + when(metadataManager.getContactPoints()).thenReturn(ImmutableSet.of(node1, node2)); + when(context.getNodeFilter(DriverExecutionProfile.DEFAULT_NAME)) + .thenReturn(node -> node.equals(node1)); + + DefaultLoadBalancingPolicy policy = + new DefaultLoadBalancingPolicy(context, DriverExecutionProfile.DEFAULT_NAME); + + // When + policy.init( + ImmutableMap.of( + UUID.randomUUID(), node1, UUID.randomUUID(), node2, UUID.randomUUID(), node3), + distanceReporter); + + // Then + verify(distanceReporter).setDistance(node1, NodeDistance.LOCAL); + verify(distanceReporter).setDistance(node2, NodeDistance.IGNORED); + verify(distanceReporter).setDistance(node3, NodeDistance.IGNORED); + assertThat(policy.localDcLiveNodes).containsExactlyInAnyOrder(node1); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyQueryPlanTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyQueryPlanTest.java new file mode 100644 index 00000000000..60d67923935 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyQueryPlanTest.java @@ -0,0 +1,196 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.loadbalancing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.TokenMap; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.internal.core.session.DefaultSession; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Optional; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class DefaultLoadBalancingPolicyQueryPlanTest extends DefaultLoadBalancingPolicyTestBase { + + private static final CqlIdentifier KEYSPACE = CqlIdentifier.fromInternal("ks"); + private static final ByteBuffer ROUTING_KEY = Bytes.fromHexString("0xdeadbeef"); + + @Mock private Request request; + @Mock private DefaultSession session; + @Mock private Metadata metadata; + @Mock private TokenMap tokenMap; + + private DefaultLoadBalancingPolicy policy; + + @Before + @Override + public void setup() { + super.setup(); + + when(metadataManager.getContactPoints()).thenReturn(ImmutableSet.of(node1)); + + when(metadataManager.getMetadata()).thenReturn(metadata); + when(metadata.getTokenMap()).thenAnswer(invocation -> Optional.of(this.tokenMap)); + + // Use a subclass to disable shuffling, we just spy to make sure that the shuffling method was + // called (makes tests easier) + policy = spy(new NonShufflingPolicy(context, DriverExecutionProfile.DEFAULT_NAME)); + policy.init( + ImmutableMap.of( + UUID.randomUUID(), node1, + UUID.randomUUID(), node2, + UUID.randomUUID(), node3, + UUID.randomUUID(), node4, + UUID.randomUUID(), node5), + distanceReporter); + + // Note: this test relies on the fact that the policy uses a CopyOnWriteArraySet which preserves + // insertion order. + assertThat(policy.localDcLiveNodes).containsExactly(node1, node2, node3, node4, node5); + } + + @Test + public void should_use_round_robin_when_request_has_no_routing_keyspace() { + // By default from Mockito: + assertThat(request.getKeyspace()).isNull(); + assertThat(request.getRoutingKeyspace()).isNull(); + + assertRoundRobinQueryPlans(); + + verify(request, never()).getRoutingKey(); + verify(request, never()).getRoutingToken(); + verify(metadataManager, never()).getMetadata(); + } + + @Test + public void should_use_round_robin_when_request_has_no_routing_key_or_token() { + when(request.getRoutingKeyspace()).thenReturn(KEYSPACE); + assertThat(request.getRoutingKey()).isNull(); + assertThat(request.getRoutingToken()).isNull(); + + assertRoundRobinQueryPlans(); + + verify(metadataManager, never()).getMetadata(); + } + + @Test + public void should_use_round_robin_when_token_map_absent() { + when(request.getRoutingKeyspace()).thenReturn(KEYSPACE); + when(request.getRoutingKey()).thenReturn(ROUTING_KEY); + + when(metadata.getTokenMap()).thenReturn(Optional.empty()); + + assertRoundRobinQueryPlans(); + + verify(metadata, atLeast(1)).getTokenMap(); + } + + @Test + public void should_use_round_robin_when_token_map_returns_no_replicas() { + when(request.getRoutingKeyspace()).thenReturn(KEYSPACE); + when(request.getRoutingKey()).thenReturn(ROUTING_KEY); + when(tokenMap.getReplicas(KEYSPACE, ROUTING_KEY)).thenReturn(Collections.emptySet()); + + assertRoundRobinQueryPlans(); + + verify(tokenMap, atLeast(1)).getReplicas(KEYSPACE, ROUTING_KEY); + } + + private void assertRoundRobinQueryPlans() { + for (int i = 0; i < 3; i++) { + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node1, node2, node3, node4, node5); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node2, node3, node4, node5, node1); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node3, node4, node5, node1, node2); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node4, node5, node1, node2, node3); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node5, node1, node2, node3, node4); + } + } + + @Test + public void should_prioritize_single_replica() { + when(request.getRoutingKeyspace()).thenReturn(KEYSPACE); + when(request.getRoutingKey()).thenReturn(ROUTING_KEY); + when(tokenMap.getReplicas(KEYSPACE, ROUTING_KEY)).thenReturn(ImmutableSet.of(node3)); + + // node3 always first, round-robin on the rest + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node3, node1, node2, node4, node5); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node3, node2, node4, node5, node1); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node3, node4, node5, node1, node2); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node3, node5, node1, node2, node4); + + // Should not shuffle replicas since there is only one + verify(policy, never()).shuffleHead(any(), anyInt()); + } + + @Test + public void should_prioritize_and_shuffle_replicas() { + when(request.getRoutingKeyspace()).thenReturn(KEYSPACE); + when(request.getRoutingKey()).thenReturn(ROUTING_KEY); + when(tokenMap.getReplicas(KEYSPACE, ROUTING_KEY)).thenReturn(ImmutableSet.of(node3, node5)); + + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node3, node5, node1, node2, node4); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node3, node5, node2, node4, node1); + assertThat(policy.newQueryPlan(request, session)) + .containsExactly(node3, node5, node4, node1, node2); + + verify(policy, times(3)).shuffleHead(any(), eq(2)); + // No power of two choices with only two replicas + verify(session, never()).getPools(); + } + + static class NonShufflingPolicy extends DefaultLoadBalancingPolicy { + NonShufflingPolicy(DriverContext context, String profileName) { + super(context, profileName); + } + + @Override + protected void shuffleHead(Object[] currentNodes, int replicaCount) { + // nothing (keep in same order) + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyTestBase.java new file mode 100644 index 00000000000..e4f648eb3af --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/loadbalancing/DefaultLoadBalancingPolicyTestBase.java @@ -0,0 +1,90 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.loadbalancing; + +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.metadata.MetadataManager; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.slf4j.LoggerFactory; + +@RunWith(MockitoJUnitRunner.class) +public abstract class DefaultLoadBalancingPolicyTestBase { + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Mock protected DefaultNode node1; + @Mock protected DefaultNode node2; + @Mock protected DefaultNode node3; + @Mock protected DefaultNode node4; + @Mock protected DefaultNode node5; + @Mock protected InternalDriverContext context; + @Mock protected DriverConfig config; + @Mock protected DriverExecutionProfile defaultProfile; + @Mock protected LoadBalancingPolicy.DistanceReporter distanceReporter; + @Mock protected Appender appender; + @Mock protected MetadataManager metadataManager; + + @Captor protected ArgumentCaptor loggingEventCaptor; + + protected Logger logger; + + @Before + public void setup() { + when(context.getSessionName()).thenReturn("test"); + when(context.getConfig()).thenReturn(config); + when(config.getProfile(DriverExecutionProfile.DEFAULT_NAME)).thenReturn(defaultProfile); + + when(defaultProfile.getString(DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER, null)) + .thenReturn("dc1"); + + when(context.getMetadataManager()).thenReturn(metadataManager); + + logger = (Logger) LoggerFactory.getLogger(DefaultLoadBalancingPolicy.class); + logger.addAppender(appender); + + for (Node node : ImmutableList.of(node1, node2, node3, node4, node5)) { + when(node.getDatacenter()).thenReturn("dc1"); + } + + when(context.getLocalDatacenter(anyString())).thenReturn(null); + } + + @After + public void teardown() { + logger.detachAppender(appender); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/AddNodeRefreshTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/AddNodeRefreshTest.java new file mode 100644 index 00000000000..52d509ada88 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/AddNodeRefreshTest.java @@ -0,0 +1,111 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class AddNodeRefreshTest { + private static final InetSocketAddress ADDRESS1 = new InetSocketAddress("127.0.0.1", 9042); + private static final InetSocketAddress ADDRESS2 = new InetSocketAddress("127.0.0.2", 9042); + + @Mock private InternalDriverContext context; + @Mock protected MetricsFactory metricsFactory; + + private DefaultNode node1; + + @Before + public void setup() { + when(context.getMetricsFactory()).thenReturn(metricsFactory); + node1 = TestNodeFactory.newNode(1, context); + } + + @Test + public void should_add_new_node() { + // Given + DefaultMetadata oldMetadata = + new DefaultMetadata( + ImmutableMap.of(node1.getHostId(), node1), Collections.emptyMap(), null); + UUID newHostId = Uuids.random(); + DefaultEndPoint newEndPoint = TestNodeFactory.newEndPoint(2); + UUID newSchemaVersion = Uuids.random(); + DefaultNodeInfo newNodeInfo = + DefaultNodeInfo.builder() + .withHostId(newHostId) + .withEndPoint(newEndPoint) + .withDatacenter("dc1") + .withRack("rack2") + .withSchemaVersion(newSchemaVersion) + .build(); + AddNodeRefresh refresh = new AddNodeRefresh(newNodeInfo); + + // When + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + + // Then + Map newNodes = result.newMetadata.getNodes(); + assertThat(newNodes).containsOnlyKeys(node1.getHostId(), newHostId); + Node node2 = newNodes.get(newHostId); + assertThat(node2.getEndPoint()).isEqualTo(newEndPoint); + assertThat(node2.getDatacenter()).isEqualTo("dc1"); + assertThat(node2.getRack()).isEqualTo("rack2"); + assertThat(node2.getHostId()).isEqualTo(newHostId); + assertThat(node2.getSchemaVersion()).isEqualTo(newSchemaVersion); + assertThat(result.events).containsExactly(NodeStateEvent.added((DefaultNode) node2)); + } + + @Test + public void should_not_add_existing_node() { + // Given + DefaultMetadata oldMetadata = + new DefaultMetadata( + ImmutableMap.of(node1.getHostId(), node1), Collections.emptyMap(), null); + DefaultNodeInfo newNodeInfo = + DefaultNodeInfo.builder() + .withHostId(node1.getHostId()) + .withEndPoint(node1.getEndPoint()) + .withDatacenter("dc1") + .withRack("rack2") + .build(); + AddNodeRefresh refresh = new AddNodeRefresh(newNodeInfo); + + // When + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + + // Then + assertThat(result.newMetadata.getNodes()).containsOnlyKeys(node1.getHostId()); + // Info is not copied over: + assertThat(node1.getDatacenter()).isNull(); + assertThat(node1.getRack()).isNull(); + assertThat(result.events).isEmpty(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/DefaultMetadataTokenMapTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/DefaultMetadataTokenMapTest.java new file mode 100644 index 00000000000..79e56e1d832 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/DefaultMetadataTokenMapTest.java @@ -0,0 +1,167 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.token.DefaultReplicationStrategyFactory; +import com.datastax.oss.driver.internal.core.metadata.token.Murmur3TokenFactory; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultMetadataTokenMapTest { + + // Simulate the simplest setup possible for a functional token map. We're not testing the token + // map itself, only how the metadata interacts with it. + private static final String TOKEN1 = "-9000000000000000000"; + private static final String TOKEN2 = "9000000000000000000"; + private static final Node NODE1 = mockNode(TOKEN1); + private static final Node NODE2 = mockNode(TOKEN2); + private static final CqlIdentifier KEYSPACE_NAME = CqlIdentifier.fromInternal("ks"); + private static final KeyspaceMetadata KEYSPACE = + mockKeyspace( + KEYSPACE_NAME, + ImmutableMap.of( + "class", "org.apache.cassandra.locator.SimpleStrategy", "replication_factor", "1")); + + @Mock private InternalDriverContext context; + + @Before + public void setup() { + DefaultReplicationStrategyFactory replicationStrategyFactory = + new DefaultReplicationStrategyFactory(context); + when(context.getReplicationStrategyFactory()).thenReturn(replicationStrategyFactory); + } + + @Test + public void should_not_build_token_map_when_initializing_with_contact_points() { + DefaultMetadata contactPointsMetadata = + new DefaultMetadata( + ImmutableMap.of(NODE1.getHostId(), NODE1), Collections.emptyMap(), null); + assertThat(contactPointsMetadata.getTokenMap()).isNotPresent(); + } + + @Test + public void should_build_minimal_token_map_on_first_refresh() { + DefaultMetadata contactPointsMetadata = + new DefaultMetadata( + ImmutableMap.of(NODE1.getHostId(), NODE1), Collections.emptyMap(), null); + DefaultMetadata firstRefreshMetadata = + contactPointsMetadata.withNodes( + ImmutableMap.of(NODE1.getHostId(), NODE1), + true, + true, + new Murmur3TokenFactory(), + context); + assertThat(firstRefreshMetadata.getTokenMap().get().getTokenRanges()).hasSize(1); + } + + @Test + public void should_not_build_token_map_when_disabled() { + DefaultMetadata contactPointsMetadata = + new DefaultMetadata( + ImmutableMap.of(NODE1.getHostId(), NODE1), Collections.emptyMap(), null); + DefaultMetadata firstRefreshMetadata = + contactPointsMetadata.withNodes( + ImmutableMap.of(NODE1.getHostId(), NODE1), + false, + true, + new Murmur3TokenFactory(), + context); + assertThat(firstRefreshMetadata.getTokenMap()).isNotPresent(); + } + + @Test + public void should_stay_empty_on_first_refresh_if_partitioner_missing() { + DefaultMetadata contactPointsMetadata = + new DefaultMetadata( + ImmutableMap.of(NODE1.getHostId(), NODE1), Collections.emptyMap(), null); + DefaultMetadata firstRefreshMetadata = + contactPointsMetadata.withNodes( + ImmutableMap.of(NODE1.getHostId(), NODE1), true, true, null, context); + assertThat(firstRefreshMetadata.getTokenMap()).isNotPresent(); + } + + @Test + public void should_update_minimal_token_map_if_new_node_and_still_no_schema() { + DefaultMetadata contactPointsMetadata = + new DefaultMetadata( + ImmutableMap.of(NODE1.getHostId(), NODE1), Collections.emptyMap(), null); + DefaultMetadata firstRefreshMetadata = + contactPointsMetadata.withNodes( + ImmutableMap.of(NODE1.getHostId(), NODE1), + true, + true, + new Murmur3TokenFactory(), + context); + DefaultMetadata secondRefreshMetadata = + firstRefreshMetadata.withNodes( + ImmutableMap.of(NODE1.getHostId(), NODE1, NODE2.getHostId(), NODE2), + true, + false, + null, + context); + assertThat(secondRefreshMetadata.getTokenMap().get().getTokenRanges()).hasSize(2); + } + + @Test + public void should_update_token_map_when_schema_changes() { + DefaultMetadata contactPointsMetadata = + new DefaultMetadata( + ImmutableMap.of(NODE1.getHostId(), NODE1), Collections.emptyMap(), null); + DefaultMetadata firstRefreshMetadata = + contactPointsMetadata.withNodes( + ImmutableMap.of(NODE1.getHostId(), NODE1), + true, + true, + new Murmur3TokenFactory(), + context); + DefaultMetadata schemaRefreshMetadata = + firstRefreshMetadata.withSchema(ImmutableMap.of(KEYSPACE_NAME, KEYSPACE), true, context); + assertThat(schemaRefreshMetadata.getTokenMap().get().getTokenRanges(KEYSPACE_NAME, NODE1)) + .isNotEmpty(); + } + + private static DefaultNode mockNode(String token) { + DefaultNode node = mock(DefaultNode.class); + when(node.getHostId()).thenReturn(UUID.randomUUID()); + when(node.getRawTokens()).thenReturn(ImmutableSet.of(token)); + return node; + } + + private static KeyspaceMetadata mockKeyspace( + CqlIdentifier name, Map replicationConfig) { + KeyspaceMetadata keyspace = mock(KeyspaceMetadata.class); + when(keyspace.getName()).thenReturn(name); + when(keyspace.getReplication()).thenReturn(replicationConfig); + return keyspace; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/DefaultTopologyMonitorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/DefaultTopologyMonitorTest.java new file mode 100644 index 00000000000..d2336ab428c --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/DefaultTopologyMonitorTest.java @@ -0,0 +1,468 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.internal.core.addresstranslation.PassThroughAddressTranslator; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.control.ControlConnection; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterators; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class DefaultTopologyMonitorTest { + + private static final InetSocketAddress ADDRESS1 = new InetSocketAddress("127.0.0.1", 9042); + private static final InetSocketAddress ADDRESS2 = new InetSocketAddress("127.0.0.2", 9042); + + @Mock private InternalDriverContext context; + @Mock private DriverConfig config; + @Mock private DriverExecutionProfile defaultConfig; + @Mock private ControlConnection controlConnection; + @Mock private DriverChannel channel; + @Mock protected MetricsFactory metricsFactory; + + private AddressTranslator addressTranslator; + private DefaultNode node1; + private DefaultNode node2; + + private TestTopologyMonitor topologyMonitor; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(context.getMetricsFactory()).thenReturn(metricsFactory); + + node1 = TestNodeFactory.newNode(1, context); + node2 = TestNodeFactory.newNode(2, context); + + when(defaultConfig.getDuration(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT)) + .thenReturn(Duration.ofSeconds(1)); + when(config.getDefaultProfile()).thenReturn(defaultConfig); + when(context.getConfig()).thenReturn(config); + + addressTranslator = spy(new PassThroughAddressTranslator(context)); + when(context.getAddressTranslator()).thenReturn(addressTranslator); + + when(channel.getEndPoint()).thenReturn(node1.getEndPoint()); + when(controlConnection.channel()).thenReturn(channel); + when(context.getControlConnection()).thenReturn(controlConnection); + + topologyMonitor = new TestTopologyMonitor(context); + } + + @Test + public void should_initialize_control_connection() { + // When + topologyMonitor.init(); + + // Then + verify(controlConnection).init(true, false, true); + } + + @Test + public void should_not_refresh_control_node() { + // When + CompletionStage> futureInfo = topologyMonitor.refreshNode(node1); + + // Then + assertThatStage(futureInfo).isSuccess(maybeInfo -> assertThat(maybeInfo.isPresent()).isFalse()); + } + + @Test + public void should_refresh_node_from_peers_if_broadcast_address_is_present() { + // Given + node2.broadcastAddress = ADDRESS2; + topologyMonitor.isSchemaV2 = false; + topologyMonitor.stubQueries( + new StubbedQuery( + "SELECT * FROM system.peers WHERE peer = :address", + ImmutableMap.of("address", ADDRESS2.getAddress()), + mockResult(mockPeersRow(2, node2.getHostId())))); + + // When + CompletionStage> futureInfo = topologyMonitor.refreshNode(node2); + + // Then + assertThatStage(futureInfo) + .isSuccess( + maybeInfo -> { + assertThat(maybeInfo.isPresent()).isTrue(); + NodeInfo info = maybeInfo.get(); + assertThat(info.getDatacenter()).isEqualTo("dc2"); + }); + } + + @Test + public void should_refresh_node_from_peers_if_broadcast_address_is_present_v2() { + // Given + node2.broadcastAddress = ADDRESS2; + topologyMonitor.isSchemaV2 = true; + topologyMonitor.stubQueries( + new StubbedQuery( + "SELECT * FROM system.peers_v2 WHERE peer = :address and peer_port = :port", + ImmutableMap.of("address", ADDRESS2.getAddress(), "peer", 9042), + mockResult(mockPeersV2Row(2, node2.getHostId())))); + + // When + CompletionStage> futureInfo = topologyMonitor.refreshNode(node2); + + // Then + assertThatStage(futureInfo) + .isSuccess( + maybeInfo -> { + assertThat(maybeInfo.isPresent()).isTrue(); + NodeInfo info = maybeInfo.get(); + assertThat(info.getDatacenter()).isEqualTo("dc2"); + assertThat(info.getBroadcastAddress().get().getPort()).isEqualTo(7002); + }); + } + + @Test + public void should_refresh_node_from_peers_if_broadcast_address_is_not_present() { + // Given + topologyMonitor.isSchemaV2 = false; + node2.broadcastAddress = null; + AdminRow peer3 = mockPeersRow(3, UUID.randomUUID()); + AdminRow peer2 = mockPeersRow(2, node2.getHostId()); + topologyMonitor.stubQueries( + new StubbedQuery("SELECT * FROM system.peers", mockResult(peer3, peer2))); + + // When + CompletionStage> futureInfo = topologyMonitor.refreshNode(node2); + + // Then + assertThatStage(futureInfo) + .isSuccess( + maybeInfo -> { + assertThat(maybeInfo.isPresent()).isTrue(); + NodeInfo info = maybeInfo.get(); + assertThat(info.getDatacenter()).isEqualTo("dc2"); + }); + // The rpc_address in each row should have been tried, only the last row should have been + // converted + verify(peer3).getUuid("host_id"); + verify(peer3, never()).getString(anyString()); + + verify(peer2, times(2)).getUuid("host_id"); + verify(peer2).getString("data_center"); + } + + @Test + public void should_refresh_node_from_peers_if_broadcast_address_is_not_present_V2() { + // Given + topologyMonitor.isSchemaV2 = true; + node2.broadcastAddress = null; + AdminRow peer3 = mockPeersV2Row(3, UUID.randomUUID()); + AdminRow peer2 = mockPeersV2Row(2, node2.getHostId()); + topologyMonitor.stubQueries( + new StubbedQuery("SELECT * FROM system.peers_v2", mockResult(peer3, peer2))); + + // When + CompletionStage> futureInfo = topologyMonitor.refreshNode(node2); + + // Then + assertThatStage(futureInfo) + .isSuccess( + maybeInfo -> { + assertThat(maybeInfo.isPresent()).isTrue(); + NodeInfo info = maybeInfo.get(); + assertThat(info.getDatacenter()).isEqualTo("dc2"); + }); + // The host_id in each row should have been tried, only the last row should have been + // converted + verify(peer3).getUuid("host_id"); + verify(peer3, never()).getString(anyString()); + + verify(peer2, times(2)).getUuid("host_id"); + verify(peer2).getString("data_center"); + } + + @Test + public void should_get_new_node_from_peers() { + // Given + AdminRow peer3 = mockPeersRow(3, UUID.randomUUID()); + AdminRow peer2 = mockPeersRow(2, node2.getHostId()); + AdminRow peer1 = mockPeersRow(1, node1.getHostId()); + topologyMonitor.isSchemaV2 = false; + topologyMonitor.stubQueries( + new StubbedQuery("SELECT * FROM system.peers", mockResult(peer3, peer2, peer1))); + + // When + CompletionStage> futureInfo = topologyMonitor.getNewNodeInfo(ADDRESS1); + + // Then + assertThatStage(futureInfo) + .isSuccess( + maybeInfo -> { + assertThat(maybeInfo.isPresent()).isTrue(); + NodeInfo info = maybeInfo.get(); + assertThat(info.getDatacenter()).isEqualTo("dc1"); + }); + // The rpc_address in each row should have been tried, only the last row should have been + // converted + verify(peer3).getInetAddress("rpc_address"); + verify(peer3, never()).getString(anyString()); + + verify(peer2).getInetAddress("rpc_address"); + verify(peer2, never()).getString(anyString()); + + verify(peer1).getInetAddress("rpc_address"); + verify(peer1).getString("data_center"); + } + + @Test + public void should_get_new_node_from_peers_v2() { + // Given + AdminRow peer3 = mockPeersV2Row(3, UUID.randomUUID()); + AdminRow peer2 = mockPeersV2Row(2, node2.getHostId()); + AdminRow peer1 = mockPeersV2Row(1, node1.getHostId()); + topologyMonitor.isSchemaV2 = true; + topologyMonitor.stubQueries( + new StubbedQuery("SELECT * FROM system.peers_v2", mockResult(peer3, peer2, peer1))); + + // When + CompletionStage> futureInfo = topologyMonitor.getNewNodeInfo(ADDRESS1); + + // Then + assertThatStage(futureInfo) + .isSuccess( + maybeInfo -> { + assertThat(maybeInfo.isPresent()).isTrue(); + NodeInfo info = maybeInfo.get(); + assertThat(info.getDatacenter()).isEqualTo("dc1"); + }); + // The natove in each row should have been tried, only the last row should have been + // converted + verify(peer3).getInetAddress("native_address"); + verify(peer3, never()).getString(anyString()); + + verify(peer2).getInetAddress("native_address"); + verify(peer2, never()).getString(anyString()); + + verify(peer1).getInetAddress("native_address"); + verify(peer1).getString("data_center"); + } + + @Test + public void should_refresh_node_list_from_local_and_peers() { + // Given + AdminRow peer3 = mockPeersRow(3, UUID.randomUUID()); + AdminRow peer2 = mockPeersRow(2, node2.getHostId()); + topologyMonitor.stubQueries( + new StubbedQuery("SELECT * FROM system.local", mockResult(mockLocalRow(1))), + new StubbedQuery( + "SELECT * FROM system.peers_v2", + Collections.emptyMap(), + mockResult(peer3, peer2), + true), + new StubbedQuery("SELECT * FROM system.peers", mockResult(peer3, peer2))); + + // When + CompletionStage> futureInfos = topologyMonitor.refreshNodeList(); + + // Then + assertThatStage(futureInfos) + .isSuccess( + infos -> { + Iterator iterator = infos.iterator(); + NodeInfo info1 = iterator.next(); + assertThat(info1.getEndPoint()).isEqualTo(node1.getEndPoint()); + assertThat(info1.getDatacenter()).isEqualTo("dc1"); + NodeInfo info3 = iterator.next(); + assertThat(info3.getEndPoint().resolve()) + .isEqualTo(new InetSocketAddress("127.0.0.3", 9042)); + assertThat(info3.getDatacenter()).isEqualTo("dc3"); + NodeInfo info2 = iterator.next(); + assertThat(info2.getEndPoint()).isEqualTo(node2.getEndPoint()); + assertThat(info2.getDatacenter()).isEqualTo("dc2"); + }); + } + + @Test + public void should_stop_executing_queries_once_closed() throws Exception { + // Given + topologyMonitor.close(); + + // When + CompletionStage> futureInfos = topologyMonitor.refreshNodeList(); + + // Then + assertThatStage(futureInfos) + .isFailed(error -> assertThat(error).isInstanceOf(IllegalStateException.class)); + } + + /** Mocks the query execution logic. */ + private static class TestTopologyMonitor extends DefaultTopologyMonitor { + + private final Queue queries = new ArrayDeque<>(); + + private TestTopologyMonitor(InternalDriverContext context) { + super(context); + port = 9042; + } + + private void stubQueries(StubbedQuery... queries) { + this.queries.addAll(Arrays.asList(queries)); + } + + @Override + protected CompletionStage query( + DriverChannel channel, String queryString, Map parameters) { + StubbedQuery nextQuery = queries.poll(); + assertThat(nextQuery).isNotNull(); + assertThat(nextQuery.queryString).isEqualTo(queryString); + assertThat(nextQuery.parameters).isEqualTo(parameters); + if (nextQuery.error) { + new CompletableFuture().completeExceptionally(new Exception("PlaceHolder")); + } + return CompletableFuture.completedFuture(nextQuery.result); + } + } + + private static class StubbedQuery { + private final String queryString; + private final Map parameters; + private final AdminResult result; + private final boolean error; + + private StubbedQuery( + String queryString, Map parameters, AdminResult result, boolean error) { + this.queryString = queryString; + this.parameters = parameters; + this.result = result; + this.error = error; + } + + private StubbedQuery(String queryString, Map parameters, AdminResult result) { + this(queryString, parameters, result, false); + } + + private StubbedQuery(String queryString, AdminResult result) { + this(queryString, Collections.emptyMap(), result); + } + + private CompletionStage throwException() throws Exception { + throw new Exception("Placeholder"); + } + } + + private AdminRow mockLocalRow(int i) { + try { + AdminRow row = mock(AdminRow.class); + when(row.getInetAddress("broadcast_address")) + .thenReturn(InetAddress.getByName("127.0.0." + i)); + when(row.getString("data_center")).thenReturn("dc" + i); + when(row.getInetAddress("listen_address")).thenReturn(InetAddress.getByName("127.0.0." + i)); + when(row.getString("rack")).thenReturn("rack" + i); + when(row.getString("release_version")).thenReturn("release_version" + i); + + // The driver should not use this column for the local row, because it can contain the + // non-broadcast RPC address. Simulate the bug to ensure it's handled correctly. + when(row.getInetAddress("rpc_address")).thenReturn(InetAddress.getByName("0.0.0.0")); + + when(row.getSetOfString("tokens")).thenReturn(ImmutableSet.of("token" + i)); + return row; + } catch (UnknownHostException e) { + fail("unexpected", e); + return null; + } + } + + private AdminRow mockPeersRow(int i, UUID hostId) { + try { + AdminRow row = mock(AdminRow.class); + when(row.getUuid("host_id")).thenReturn(hostId); + when(row.getInetAddress("peer")).thenReturn(InetAddress.getByName("127.0.0." + i)); + when(row.getString("data_center")).thenReturn("dc" + i); + when(row.getString("rack")).thenReturn("rack" + i); + when(row.getString("release_version")).thenReturn("release_version" + i); + when(row.getInetAddress("rpc_address")).thenReturn(InetAddress.getByName("127.0.0." + i)); + when(row.getSetOfString("tokens")).thenReturn(ImmutableSet.of("token" + i)); + return row; + } catch (UnknownHostException e) { + fail("unexpected", e); + return null; + } + } + + private AdminRow mockPeersV2Row(int i, UUID hostId) { + try { + AdminRow row = mock(AdminRow.class); + when(row.getUuid("host_id")).thenReturn(hostId); + when(row.getInetAddress("peer")).thenReturn(InetAddress.getByName("127.0.0." + i)); + when(row.getInteger("peer_port")).thenReturn(7000 + i); + when(row.getString("data_center")).thenReturn("dc" + i); + when(row.getString("rack")).thenReturn("rack" + i); + when(row.getString("release_version")).thenReturn("release_version" + i); + when(row.getInetAddress("native_address")).thenReturn(InetAddress.getByName("127.0.0." + i)); + when(row.getInteger("native_port")).thenReturn(9042); + when(row.getSetOfString("tokens")).thenReturn(ImmutableSet.of("token" + i)); + when(row.contains("peer_port")).thenReturn(true); + when(row.contains("native_port")).thenReturn(true); + return row; + } catch (UnknownHostException e) { + fail("unexpected", e); + return null; + } + } + + private AdminResult mockResult(AdminRow... rows) { + AdminResult result = mock(AdminResult.class); + when(result.iterator()).thenReturn(Iterators.forArray(rows)); + return result; + } + + private AdminResult errorResult() throws Exception { + throw new Exception("Boiler plate Exception"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/FullNodeListRefreshTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/FullNodeListRefreshTest.java new file mode 100644 index 00000000000..1d7b0b0d02f --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/FullNodeListRefreshTest.java @@ -0,0 +1,126 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.Collections; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class FullNodeListRefreshTest { + + @Mock private InternalDriverContext context; + @Mock protected MetricsFactory metricsFactory; + + private DefaultNode node1; + private DefaultNode node2; + private DefaultNode node3; + + @Before + public void setup() { + when(context.getMetricsFactory()).thenReturn(metricsFactory); + + node1 = TestNodeFactory.newNode(1, context); + node2 = TestNodeFactory.newNode(2, context); + node3 = TestNodeFactory.newNode(3, context); + } + + @Test + public void should_add_and_remove_nodes() { + // Given + DefaultMetadata oldMetadata = + new DefaultMetadata( + ImmutableMap.of(node1.getHostId(), node1, node2.getHostId(), node2), + Collections.emptyMap(), + null); + Iterable newInfos = + ImmutableList.of( + DefaultNodeInfo.builder() + .withEndPoint(node2.getEndPoint()) + .withHostId(node2.getHostId()) + .build(), + DefaultNodeInfo.builder() + .withEndPoint(node3.getEndPoint()) + .withHostId(node3.getHostId()) + .build()); + FullNodeListRefresh refresh = new FullNodeListRefresh(newInfos); + + // When + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + + // Then + assertThat(result.newMetadata.getNodes()) + .containsOnlyKeys(node2.getHostId(), node3.getHostId()); + assertThat(result.events) + .containsOnly(NodeStateEvent.removed(node1), NodeStateEvent.added(node3)); + } + + @Test + public void should_update_existing_nodes() { + // Given + DefaultMetadata oldMetadata = + new DefaultMetadata( + ImmutableMap.of(node1.getHostId(), node1, node2.getHostId(), node2), + Collections.emptyMap(), + null); + + UUID schemaVersion1 = Uuids.random(); + UUID schemaVersion2 = Uuids.random(); + Iterable newInfos = + ImmutableList.of( + DefaultNodeInfo.builder() + .withEndPoint(node1.getEndPoint()) + .withDatacenter("dc1") + .withRack("rack1") + .withHostId(node1.getHostId()) + .withSchemaVersion(schemaVersion1) + .build(), + DefaultNodeInfo.builder() + .withEndPoint(node2.getEndPoint()) + .withDatacenter("dc1") + .withRack("rack2") + .withHostId(node2.getHostId()) + .withSchemaVersion(schemaVersion2) + .build()); + FullNodeListRefresh refresh = new FullNodeListRefresh(newInfos); + + // When + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + + // Then + assertThat(result.newMetadata.getNodes()) + .containsOnlyKeys(node1.getHostId(), node2.getHostId()); + assertThat(node1.getDatacenter()).isEqualTo("dc1"); + assertThat(node1.getRack()).isEqualTo("rack1"); + assertThat(node1.getSchemaVersion()).isEqualTo(schemaVersion1); + assertThat(node2.getDatacenter()).isEqualTo("dc1"); + assertThat(node2.getRack()).isEqualTo("rack2"); + assertThat(node2.getSchemaVersion()).isEqualTo(schemaVersion2); + assertThat(result.events).isEmpty(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapperTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapperTest.java new file mode 100644 index 00000000000..d7be8e96b0b --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/LoadBalancingPolicyWrapperTest.java @@ -0,0 +1,280 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy.DistanceReporter; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +@RunWith(MockitoJUnitRunner.class) +public class LoadBalancingPolicyWrapperTest { + + private DefaultNode node1; + private DefaultNode node2; + private DefaultNode node3; + + private Map allNodes; + private Set contactPoints; + private Queue defaultPolicysQueryPlan; + + @Mock private InternalDriverContext context; + @Mock private LoadBalancingPolicy policy1; + @Mock private LoadBalancingPolicy policy2; + @Mock private LoadBalancingPolicy policy3; + private EventBus eventBus; + @Mock private MetadataManager metadataManager; + @Mock private Metadata metadata; + @Mock protected MetricsFactory metricsFactory; + @Captor private ArgumentCaptor> initNodesCaptor; + + private LoadBalancingPolicyWrapper wrapper; + + @Before + public void setup() { + when(context.getMetricsFactory()).thenReturn(metricsFactory); + + node1 = TestNodeFactory.newNode(1, context); + node2 = TestNodeFactory.newNode(2, context); + node3 = TestNodeFactory.newNode(3, context); + + contactPoints = ImmutableSet.of(node1, node2); + allNodes = + ImmutableMap.of( + node1.getHostId(), node1, node2.getHostId(), node2, node3.getHostId(), node3); + when(metadataManager.getMetadata()).thenReturn(metadata); + when(metadata.getNodes()).thenReturn(allNodes); + when(metadataManager.getContactPoints()).thenReturn(contactPoints); + when(context.getMetadataManager()).thenReturn(metadataManager); + + defaultPolicysQueryPlan = Lists.newLinkedList(ImmutableList.of(node3, node2, node1)); + when(policy1.newQueryPlan(null, null)).thenReturn(defaultPolicysQueryPlan); + + eventBus = spy(new EventBus("test")); + when(context.getEventBus()).thenReturn(eventBus); + + wrapper = + new LoadBalancingPolicyWrapper( + context, + ImmutableMap.of( + DriverExecutionProfile.DEFAULT_NAME, + policy1, + "profile1", + policy1, + "profile2", + policy2, + "profile3", + policy3)); + } + + @Test + public void should_build_query_plan_from_contact_points_before_init() { + // When + Queue queryPlan = wrapper.newQueryPlan(); + + // Then + for (LoadBalancingPolicy policy : ImmutableList.of(policy1, policy2, policy3)) { + verify(policy, never()).newQueryPlan(null, null); + } + assertThat(queryPlan).containsOnlyElementsOf(contactPoints); + } + + @Test + public void should_fetch_query_plan_from_policy_after_init() { + // Given + wrapper.init(); + for (LoadBalancingPolicy policy : ImmutableList.of(policy1, policy2, policy3)) { + verify(policy).init(anyMap(), any(DistanceReporter.class)); + } + + // When + Queue queryPlan = wrapper.newQueryPlan(); + + // Then + // no-arg newQueryPlan() uses the default profile + verify(policy1).newQueryPlan(null, null); + assertThat(queryPlan).isEqualTo(defaultPolicysQueryPlan); + } + + @Test + public void should_init_policies_with_all_nodes() { + // Given + node1.state = NodeState.UP; + node2.state = NodeState.UNKNOWN; + node3.state = NodeState.DOWN; + + // When + wrapper.init(); + + // Then + for (LoadBalancingPolicy policy : ImmutableList.of(policy1, policy2, policy3)) { + verify(policy).init(initNodesCaptor.capture(), any(DistanceReporter.class)); + Map initNodes = initNodesCaptor.getValue(); + assertThat(initNodes.values()).containsOnly(node1, node2, node3); + } + } + + @Test + public void should_propagate_distances_from_policies() { + // Given + wrapper.init(); + ArgumentCaptor captor1 = ArgumentCaptor.forClass(DistanceReporter.class); + verify(policy1).init(anyMap(), captor1.capture()); + DistanceReporter distanceReporter1 = captor1.getValue(); + ArgumentCaptor captor2 = ArgumentCaptor.forClass(DistanceReporter.class); + verify(policy2).init(anyMap(), captor2.capture()); + DistanceReporter distanceReporter2 = captor1.getValue(); + ArgumentCaptor captor3 = ArgumentCaptor.forClass(DistanceReporter.class); + verify(policy3).init(anyMap(), captor3.capture()); + DistanceReporter distanceReporter3 = captor3.getValue(); + + InOrder inOrder = inOrder(eventBus); + + // When + distanceReporter1.setDistance(node1, NodeDistance.REMOTE); + + // Then + // first event defines the distance + inOrder.verify(eventBus).fire(new DistanceEvent(NodeDistance.REMOTE, node1)); + + // When + distanceReporter2.setDistance(node1, NodeDistance.REMOTE); + + // Then + // event is ignored if the node is already at this distance + inOrder.verify(eventBus, times(0)).fire(any(DistanceEvent.class)); + + // When + distanceReporter2.setDistance(node1, NodeDistance.LOCAL); + + // Then + // event is applied if it sets a smaller distance + inOrder.verify(eventBus).fire(new DistanceEvent(NodeDistance.LOCAL, node1)); + + // When + distanceReporter3.setDistance(node1, NodeDistance.IGNORED); + + // Then + // event is ignored if the node is already at a closer distance + inOrder.verify(eventBus, times(0)).fire(any(DistanceEvent.class)); + } + + @Test + public void should_not_propagate_node_states_to_policies_until_init() { + // When + eventBus.fire(NodeStateEvent.changed(NodeState.UNKNOWN, NodeState.UP, node1)); + + // Then + for (LoadBalancingPolicy policy : ImmutableList.of(policy1, policy2, policy3)) { + verify(policy, never()).onUp(node1); + } + } + + @Test + public void should_propagate_node_states_to_policies_after_init() { + // Given + wrapper.init(); + + // When + eventBus.fire(NodeStateEvent.changed(NodeState.UNKNOWN, NodeState.UP, node1)); + + // Then + for (LoadBalancingPolicy policy : ImmutableList.of(policy1, policy2, policy3)) { + verify(policy).onUp(node1); + } + } + + @Test + public void should_accumulate_events_during_init_and_replay() throws InterruptedException { + // Given + // Hack to obtain concurrency: the main thread blocks in init, while another thread fires an + // event on the bus + CountDownLatch eventLatch = new CountDownLatch(3); + CountDownLatch initLatch = new CountDownLatch(1); + Answer mockInit = + i -> { + eventLatch.countDown(); + initLatch.await(500, TimeUnit.MILLISECONDS); + return null; + }; + for (LoadBalancingPolicy policy : ImmutableList.of(policy1, policy2, policy3)) { + doAnswer(mockInit).when(policy).init(anyMap(), any(DistanceReporter.class)); + } + + // When + Runnable runnable = + () -> { + try { + eventLatch.await(500, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + eventBus.fire(NodeStateEvent.changed(NodeState.UNKNOWN, NodeState.DOWN, node1)); + initLatch.countDown(); + }; + Thread thread = new Thread(runnable); + thread.start(); + wrapper.init(); + + // Then + // wait for init launch to signal that runnable is complete. + initLatch.await(500, TimeUnit.MILLISECONDS); + for (LoadBalancingPolicy policy : ImmutableList.of(policy1, policy2, policy3)) { + verify(policy).onDown(node1); + } + if (thread.isAlive()) { + // thread still completing - sleep to allow thread to complete. + Thread.sleep(500); + } + assertThat(thread.isAlive()).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/MetadataManagerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/MetadataManagerTest.java new file mode 100644 index 00000000000..12dcdb033e2 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/MetadataManagerTest.java @@ -0,0 +1,315 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.metadata.schema.parsing.SchemaParserFactory; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaQueriesFactory; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.Uninterruptibles; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.util.concurrent.Future; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class MetadataManagerTest { + + // Don't use 1 because that's the default when no contact points are provided + private static final EndPoint END_POINT2 = TestNodeFactory.newEndPoint(2); + private static final EndPoint END_POINT3 = TestNodeFactory.newEndPoint(3); + + @Mock private InternalDriverContext context; + @Mock private NettyOptions nettyOptions; + @Mock private TopologyMonitor topologyMonitor; + @Mock private DriverConfig config; + @Mock private DriverExecutionProfile defaultProfile; + @Mock private EventBus eventBus; + @Mock private SchemaQueriesFactory schemaQueriesFactory; + @Mock private SchemaParserFactory schemaParserFactory; + @Mock protected MetricsFactory metricsFactory; + + private DefaultEventLoopGroup adminEventLoopGroup; + + private TestMetadataManager metadataManager; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + adminEventLoopGroup = new DefaultEventLoopGroup(1); + when(nettyOptions.adminEventExecutorGroup()).thenReturn(adminEventLoopGroup); + when(context.getNettyOptions()).thenReturn(nettyOptions); + + when(context.getTopologyMonitor()).thenReturn(topologyMonitor); + + when(defaultProfile.getDuration(DefaultDriverOption.METADATA_SCHEMA_WINDOW)) + .thenReturn(Duration.ZERO); + when(defaultProfile.getInt(DefaultDriverOption.METADATA_SCHEMA_MAX_EVENTS)).thenReturn(1); + when(config.getDefaultProfile()).thenReturn(defaultProfile); + when(context.getConfig()).thenReturn(config); + + when(context.getEventBus()).thenReturn(eventBus); + when(context.getSchemaQueriesFactory()).thenReturn(schemaQueriesFactory); + when(context.getSchemaParserFactory()).thenReturn(schemaParserFactory); + + when(context.getMetricsFactory()).thenReturn(metricsFactory); + + metadataManager = new TestMetadataManager(context); + } + + @After + public void teardown() { + adminEventLoopGroup.shutdownGracefully(100, 200, TimeUnit.MILLISECONDS); + } + + @Test + public void should_add_contact_points() { + // When + metadataManager.addContactPoints(ImmutableSet.of(END_POINT2)); + + // Then + assertThat(metadataManager.getContactPoints()) + .extracting(Node::getEndPoint) + .containsOnly(END_POINT2); + assertThat(metadataManager.wasImplicitContactPoint()).isFalse(); + } + + @Test + public void should_use_default_if_no_contact_points_provided() { + // When + metadataManager.addContactPoints(Collections.emptySet()); + + // Then + assertThat(metadataManager.getContactPoints()) + .extracting(Node::getEndPoint) + .containsOnly(MetadataManager.DEFAULT_CONTACT_POINT); + assertThat(metadataManager.wasImplicitContactPoint()).isTrue(); + } + + @Test + public void should_copy_contact_points_on_refresh_of_all_nodes() { + // Given + // Run previous scenario to trigger the addition of the default contact point: + should_use_default_if_no_contact_points_provided(); + + NodeInfo info1 = mock(NodeInfo.class); + NodeInfo info2 = mock(NodeInfo.class); + List infos = ImmutableList.of(info1, info2); + when(topologyMonitor.refreshNodeList()).thenReturn(CompletableFuture.completedFuture(infos)); + + // When + CompletionStage refreshNodesFuture = metadataManager.refreshNodes(); + waitForPendingAdminTasks(); + + // Then + assertThatStage(refreshNodesFuture).isSuccess(); + assertThat(metadataManager.refreshes).hasSize(1); + InitialNodeListRefresh refresh = (InitialNodeListRefresh) metadataManager.refreshes.get(0); + assertThat(refresh.contactPoints) + .extracting(Node::getEndPoint) + .containsOnly(MetadataManager.DEFAULT_CONTACT_POINT); + assertThat(refresh.nodeInfos).containsExactlyInAnyOrder(info1, info2); + } + + @Test + public void should_refresh_all_nodes() { + // Given + // Run previous scenario to trigger the addition of the default contact point and a first + // refresh: + should_copy_contact_points_on_refresh_of_all_nodes(); + // Discard that first refresh, we don't really care about it in the context of this test, only + // that the next one won't be the first + metadataManager.refreshes.clear(); + + NodeInfo info1 = mock(NodeInfo.class); + NodeInfo info2 = mock(NodeInfo.class); + List infos = ImmutableList.of(info1, info2); + when(topologyMonitor.refreshNodeList()).thenReturn(CompletableFuture.completedFuture(infos)); + + // When + CompletionStage refreshNodesFuture = metadataManager.refreshNodes(); + waitForPendingAdminTasks(); + + // Then + assertThatStage(refreshNodesFuture).isSuccess(); + assertThat(metadataManager.refreshes).hasSize(1); + FullNodeListRefresh refresh = (FullNodeListRefresh) metadataManager.refreshes.get(0); + assertThat(refresh.nodeInfos).containsExactlyInAnyOrder(info1, info2); + } + + @Test + public void should_refresh_single_node() { + // Given + Node node = TestNodeFactory.newNode(2, context); + NodeInfo info = mock(NodeInfo.class); + when(info.getDatacenter()).thenReturn("dc1"); + when(topologyMonitor.refreshNode(node)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(info))); + + // When + CompletionStage refreshNodeFuture = metadataManager.refreshNode(node); + + // Then + // the info should have been copied to the node + assertThatStage(refreshNodeFuture).isSuccess(); + verify(info, timeout(500)).getDatacenter(); + assertThat(node.getDatacenter()).isEqualTo("dc1"); + } + + @Test + public void should_ignore_node_refresh_if_topology_monitor_does_not_have_info() { + // Given + Node node = mock(Node.class); + when(topologyMonitor.refreshNode(node)) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + // When + CompletionStage refreshNodeFuture = metadataManager.refreshNode(node); + + // Then + assertThatStage(refreshNodeFuture).isSuccess(); + } + + @Test + public void should_add_node() { + // Given + InetSocketAddress broadcastRpcAddress = ((InetSocketAddress) END_POINT2.resolve()); + NodeInfo info = mock(NodeInfo.class); + when(info.getBroadcastRpcAddress()).thenReturn(Optional.of(broadcastRpcAddress)); + when(topologyMonitor.getNewNodeInfo(broadcastRpcAddress)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(info))); + + // When + metadataManager.addNode(broadcastRpcAddress); + waitForPendingAdminTasks(); + + // Then + assertThat(metadataManager.refreshes).hasSize(1); + AddNodeRefresh refresh = (AddNodeRefresh) metadataManager.refreshes.get(0); + assertThat(refresh.newNodeInfo).isEqualTo(info); + } + + @Test + public void should_not_add_node_if_broadcast_rpc_address_does_not_match() { + // Given + InetSocketAddress broadcastRpcAddress2 = ((InetSocketAddress) END_POINT2.resolve()); + InetSocketAddress broadcastRpcAddress3 = ((InetSocketAddress) END_POINT3.resolve()); + NodeInfo info = mock(NodeInfo.class); + when(topologyMonitor.getNewNodeInfo(broadcastRpcAddress2)) + .thenReturn(CompletableFuture.completedFuture(Optional.of(info))); + when(info.getBroadcastRpcAddress()) + .thenReturn( + Optional.of(broadcastRpcAddress3) // Does not match the address we got the info with + ); + + // When + metadataManager.addNode(broadcastRpcAddress2); + waitForPendingAdminTasks(); + + // Then + assertThat(metadataManager.refreshes).isEmpty(); + } + + @Test + public void should_not_add_node_if_topology_monitor_does_not_have_info() { + // Given + InetSocketAddress broadcastRpcAddress2 = ((InetSocketAddress) END_POINT2.resolve()); + when(topologyMonitor.getNewNodeInfo(broadcastRpcAddress2)) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + // When + metadataManager.addNode(broadcastRpcAddress2); + waitForPendingAdminTasks(); + + // Then + assertThat(metadataManager.refreshes).isEmpty(); + } + + @Test + public void should_remove_node() { + // Given + InetSocketAddress broadcastRpcAddress2 = ((InetSocketAddress) END_POINT2.resolve()); + + // When + metadataManager.removeNode(broadcastRpcAddress2); + waitForPendingAdminTasks(); + + // Then + assertThat(metadataManager.refreshes).hasSize(1); + RemoveNodeRefresh refresh = (RemoveNodeRefresh) metadataManager.refreshes.get(0); + assertThat(refresh.broadcastRpcAddressToRemove).isEqualTo(broadcastRpcAddress2); + } + + private static class TestMetadataManager extends MetadataManager { + + private List refreshes = new CopyOnWriteArrayList<>(); + + public TestMetadataManager(InternalDriverContext context) { + super(context); + } + + @Override + Void apply(MetadataRefresh refresh) { + // Do not execute refreshes, just store them for inspection in the test + refreshes.add(refresh); + return null; + } + } + + // Wait for all the tasks on the pool's admin executor to complete. + private void waitForPendingAdminTasks() { + // This works because the event loop group is single-threaded + Future f = adminEventLoopGroup.schedule(() -> null, 5, TimeUnit.NANOSECONDS); + try { + Uninterruptibles.getUninterruptibly(f, 100, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + fail("unexpected error", e.getCause()); + } catch (TimeoutException e) { + fail("timed out while waiting for admin tasks to complete", e); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/NodeStateManagerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/NodeStateManagerTest.java new file mode 100644 index 00000000000..347185bce80 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/NodeStateManagerTest.java @@ -0,0 +1,544 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.internal.core.channel.ChannelEvent; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.internal.core.util.concurrent.BlockingOperation; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.Uninterruptibles; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.util.concurrent.Future; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.Collections; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class NodeStateManagerTest { + private static final InetSocketAddress NEW_ADDRESS = new InetSocketAddress("127.0.0.3", 9042); + + @Mock private InternalDriverContext context; + @Mock private DriverConfig config; + @Mock private DriverExecutionProfile defaultProfile; + @Mock private NettyOptions nettyOptions; + @Mock private MetadataManager metadataManager; + @Mock protected MetricsFactory metricsFactory; + private DefaultNode node1, node2; + private EventBus eventBus; + private DefaultEventLoopGroup adminEventLoopGroup; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + // Disable debouncing by default, tests that need it will override + when(defaultProfile.getDuration(DefaultDriverOption.METADATA_TOPOLOGY_WINDOW)) + .thenReturn(Duration.ofSeconds(0)); + when(defaultProfile.getInt(DefaultDriverOption.METADATA_TOPOLOGY_MAX_EVENTS)).thenReturn(1); + when(config.getDefaultProfile()).thenReturn(defaultProfile); + when(context.getConfig()).thenReturn(config); + + this.eventBus = spy(new EventBus("test")); + when(context.getEventBus()).thenReturn(eventBus); + + adminEventLoopGroup = new DefaultEventLoopGroup(1, new BlockingOperation.SafeThreadFactory()); + when(nettyOptions.adminEventExecutorGroup()).thenReturn(adminEventLoopGroup); + when(context.getNettyOptions()).thenReturn(nettyOptions); + + when(context.getMetricsFactory()).thenReturn(metricsFactory); + node1 = TestNodeFactory.newNode(1, context); + node2 = TestNodeFactory.newNode(2, context); + ImmutableMap nodes = + ImmutableMap.builder() + .put(node1.getHostId(), node1) + .put(node2.getHostId(), node2) + .build(); + Metadata metadata = new DefaultMetadata(nodes, Collections.emptyMap(), null); + when(metadataManager.getMetadata()).thenReturn(metadata); + when(metadataManager.refreshNode(any(Node.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + when(context.getMetadataManager()).thenReturn(metadataManager); + } + + @After + public void teardown() { + adminEventLoopGroup.shutdownGracefully(100, 200, TimeUnit.MILLISECONDS); + } + + @Test + public void should_ignore_up_event_if_node_is_already_up_or_forced_down() { + new NodeStateManager(context); + + for (NodeState oldState : ImmutableList.of(NodeState.UP, NodeState.FORCED_DOWN)) { + // Given + node1.state = oldState; + + // When + eventBus.fire(TopologyEvent.suggestUp(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(oldState); + } + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + } + + @Test + public void should_apply_up_event_if_node_is_unknown_or_down() { + new NodeStateManager(context); + + int i = 0; + for (NodeState oldState : ImmutableList.of(NodeState.UNKNOWN, NodeState.DOWN)) { + // Given + node1.state = oldState; + + // When + eventBus.fire(TopologyEvent.suggestUp(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(NodeState.UP); + if (oldState != NodeState.UNKNOWN) { + verify(metadataManager, times(++i)).refreshNode(node1); + } + verify(eventBus).fire(NodeStateEvent.changed(oldState, NodeState.UP, node1)); + } + } + + @Test + public void should_add_node_if_up_event_and_not_in_metadata() { + // Given + new NodeStateManager(context); + + // When + eventBus.fire(TopologyEvent.suggestUp(NEW_ADDRESS)); + waitForPendingAdminTasks(); + + // Then + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + verify(metadataManager).addNode(NEW_ADDRESS); + } + + @Test + public void should_ignore_down_event_if_node_is_down_or_forced_down() { + new NodeStateManager(context); + + for (NodeState oldState : ImmutableList.of(NodeState.DOWN, NodeState.FORCED_DOWN)) { + // Given + node1.state = oldState; + + // When + eventBus.fire(TopologyEvent.suggestDown(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(oldState); + } + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + } + + @Test + public void should_ignore_down_event_if_node_has_active_connections() { + new NodeStateManager(context); + node1.state = NodeState.UP; + eventBus.fire(ChannelEvent.channelOpened(node1)); + waitForPendingAdminTasks(); + assertThat(node1.openConnections).isEqualTo(1); + + // When + eventBus.fire(TopologyEvent.suggestDown(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(NodeState.UP); + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + } + + @Test + public void should_apply_down_event_if_node_has_no_active_connections() { + new NodeStateManager(context); + + for (NodeState oldState : ImmutableList.of(NodeState.UP, NodeState.UNKNOWN)) { + // Given + node1.state = oldState; + assertThat(node1.openConnections).isEqualTo(0); + + // When + eventBus.fire(TopologyEvent.suggestDown(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(NodeState.DOWN); + verify(eventBus).fire(NodeStateEvent.changed(oldState, NodeState.DOWN, node1)); + } + } + + @Test + public void should_ignore_down_event_if_not_in_metadata() { + // Given + new NodeStateManager(context); + + // When + eventBus.fire(TopologyEvent.suggestDown(NEW_ADDRESS)); + waitForPendingAdminTasks(); + + // Then + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + verify(metadataManager, never()).addNode(NEW_ADDRESS); + } + + @Test + public void should_ignore_force_down_event_if_already_forced_down() { + // Given + new NodeStateManager(context); + node1.state = NodeState.FORCED_DOWN; + + // When + eventBus.fire(TopologyEvent.forceDown(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(NodeState.FORCED_DOWN); + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + } + + @Test + public void should_apply_force_down_event_over_any_other_state() { + new NodeStateManager(context); + + for (NodeState oldState : ImmutableList.of(NodeState.UNKNOWN, NodeState.DOWN, NodeState.UP)) { + // Given + node1.state = oldState; + + // When + eventBus.fire(TopologyEvent.forceDown(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(NodeState.FORCED_DOWN); + verify(eventBus).fire(NodeStateEvent.changed(oldState, NodeState.FORCED_DOWN, node1)); + } + } + + @Test + public void should_ignore_force_down_event_if_not_in_metadata() { + // Given + new NodeStateManager(context); + + // When + eventBus.fire(TopologyEvent.forceDown(NEW_ADDRESS)); + waitForPendingAdminTasks(); + + // Then + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + verify(metadataManager, never()).addNode(NEW_ADDRESS); + } + + @Test + public void should_ignore_force_up_event_if_node_is_already_up() { + // Given + new NodeStateManager(context); + node1.state = NodeState.UP; + + // When + eventBus.fire(TopologyEvent.forceUp(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(NodeState.UP); + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + } + + @Test + public void should_apply_force_up_event_if_node_is_not_up() { + new NodeStateManager(context); + + int i = 0; + for (NodeState oldState : + ImmutableList.of(NodeState.UNKNOWN, NodeState.DOWN, NodeState.FORCED_DOWN)) { + // Given + node1.state = oldState; + + // When + eventBus.fire(TopologyEvent.forceUp(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(NodeState.UP); + verify(eventBus).fire(NodeStateEvent.changed(oldState, NodeState.UP, node1)); + if (oldState != NodeState.UNKNOWN) { + verify(metadataManager, times(++i)).refreshNode(node1); + } + } + } + + @Test + public void should_add_node_if_force_up_and_not_in_metadata() { + // Given + new NodeStateManager(context); + + // When + eventBus.fire(TopologyEvent.forceUp(NEW_ADDRESS)); + waitForPendingAdminTasks(); + + // Then + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + verify(metadataManager).addNode(NEW_ADDRESS); + } + + @Test + public void should_notify_metadata_of_node_addition() { + // Given + new NodeStateManager(context); + InetSocketAddress newAddress = NEW_ADDRESS; + + // When + eventBus.fire(TopologyEvent.suggestAdded(newAddress)); + waitForPendingAdminTasks(); + + // Then + verify(metadataManager).addNode(newAddress); + } + + @Test + public void should_ignore_addition_of_existing_node() { + // Given + new NodeStateManager(context); + + // When + eventBus.fire(TopologyEvent.suggestAdded(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + verify(metadataManager, never()).addNode(any(InetSocketAddress.class)); + } + + @Test + public void should_notify_metadata_of_node_removal() { + // Given + new NodeStateManager(context); + + // When + eventBus.fire(TopologyEvent.suggestRemoved(node1.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + verify(metadataManager).removeNode(node1.getBroadcastRpcAddress().get()); + } + + @Test + public void should_ignore_removal_of_nonexistent_node() { + // Given + new NodeStateManager(context); + InetSocketAddress newAddress = NEW_ADDRESS; + + // When + eventBus.fire(TopologyEvent.suggestRemoved(newAddress)); + waitForPendingAdminTasks(); + + // Then + verify(metadataManager, never()).removeNode(any(InetSocketAddress.class)); + } + + @Test + public void should_coalesce_topology_events() { + // Given + when(defaultProfile.getDuration(DefaultDriverOption.METADATA_TOPOLOGY_WINDOW)) + .thenReturn(Duration.ofDays(1)); + when(defaultProfile.getInt(DefaultDriverOption.METADATA_TOPOLOGY_MAX_EVENTS)).thenReturn(5); + new NodeStateManager(context); + node1.state = NodeState.FORCED_DOWN; + node2.state = NodeState.DOWN; + + // When + eventBus.fire(TopologyEvent.suggestDown(node1.getBroadcastRpcAddress().get())); + eventBus.fire(TopologyEvent.forceUp(node1.getBroadcastRpcAddress().get())); + eventBus.fire(TopologyEvent.suggestDown(node2.getBroadcastRpcAddress().get())); + eventBus.fire(TopologyEvent.suggestDown(node1.getBroadcastRpcAddress().get())); + eventBus.fire(TopologyEvent.suggestUp(node2.getBroadcastRpcAddress().get())); + waitForPendingAdminTasks(); + + // Then + // down / forceUp / down => keep the last forced event => forceUp + assertThat(node1.state).isEqualTo(NodeState.UP); + // down / up => keep the last => up + assertThat(node2.state).isEqualTo(NodeState.UP); + } + + @Test + public void should_track_open_connections() { + new NodeStateManager(context); + + assertThat(node1.openConnections).isEqualTo(0); + + eventBus.fire(ChannelEvent.channelOpened(node1)); + eventBus.fire(ChannelEvent.channelOpened(node1)); + waitForPendingAdminTasks(); + assertThat(node1.openConnections).isEqualTo(2); + + eventBus.fire(ChannelEvent.channelClosed(node1)); + waitForPendingAdminTasks(); + assertThat(node1.openConnections).isEqualTo(1); + } + + @Test + public void should_mark_node_up_if_down_or_unknown_and_connection_opened() { + new NodeStateManager(context); + + for (NodeState oldState : ImmutableList.of(NodeState.DOWN, NodeState.UNKNOWN)) { + // Given + node1.state = oldState; + + // When + eventBus.fire(ChannelEvent.channelOpened(node1)); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(NodeState.UP); + verify(eventBus).fire(NodeStateEvent.changed(oldState, NodeState.UP, node1)); + } + } + + @Test + public void should_not_mark_node_up_if_forced_down_and_connection_opened() { + // Given + new NodeStateManager(context); + node1.state = NodeState.FORCED_DOWN; + + // When + eventBus.fire(ChannelEvent.channelOpened(node1)); + waitForPendingAdminTasks(); + + // Then + assertThat(node1.state).isEqualTo(NodeState.FORCED_DOWN); + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + } + + @Test + public void should_track_reconnections() { + new NodeStateManager(context); + + assertThat(node1.reconnections).isEqualTo(0); + + eventBus.fire(ChannelEvent.reconnectionStarted(node1)); + eventBus.fire(ChannelEvent.reconnectionStarted(node1)); + waitForPendingAdminTasks(); + assertThat(node1.reconnections).isEqualTo(2); + + eventBus.fire(ChannelEvent.reconnectionStopped(node1)); + waitForPendingAdminTasks(); + assertThat(node1.reconnections).isEqualTo(1); + } + + @Test + public void should_mark_node_down_if_reconnection_starts_with_no_connections() { + new NodeStateManager(context); + + node1.state = NodeState.UP; + node1.openConnections = 1; + + eventBus.fire(ChannelEvent.channelClosed(node1)); + eventBus.fire(ChannelEvent.reconnectionStarted(node1)); + waitForPendingAdminTasks(); + + assertThat(node1.state).isEqualTo(NodeState.DOWN); + verify(eventBus).fire(NodeStateEvent.changed(NodeState.UP, NodeState.DOWN, node1)); + } + + @Test + public void should_mark_node_down_if_no_connections_and_reconnection_already_started() { + new NodeStateManager(context); + + node1.state = NodeState.UP; + node1.openConnections = 1; + + eventBus.fire(ChannelEvent.reconnectionStarted(node1)); + eventBus.fire(ChannelEvent.channelClosed(node1)); + waitForPendingAdminTasks(); + + assertThat(node1.state).isEqualTo(NodeState.DOWN); + verify(eventBus).fire(NodeStateEvent.changed(NodeState.UP, NodeState.DOWN, node1)); + } + + @Test + public void should_keep_node_up_if_reconnection_starts_with_some_connections() { + new NodeStateManager(context); + + node1.state = NodeState.UP; + node1.openConnections = 2; + + eventBus.fire(ChannelEvent.channelClosed(node1)); + eventBus.fire(ChannelEvent.reconnectionStarted(node1)); + waitForPendingAdminTasks(); + + assertThat(node1.state).isEqualTo(NodeState.UP); + verify(eventBus, never()).fire(any(NodeStateEvent.class)); + } + + @Test + public void should_ignore_events_when_closed() throws Exception { + NodeStateManager manager = new NodeStateManager(context); + assertThat(node1.reconnections).isEqualTo(0); + + manager.close(); + + eventBus.fire(ChannelEvent.reconnectionStarted(node1)); + waitForPendingAdminTasks(); + + assertThat(node1.reconnections).isEqualTo(0); + } + + // Wait for all the tasks on the pool's admin executor to complete. + private void waitForPendingAdminTasks() { + // This works because the event loop group is single-threaded + Future f = adminEventLoopGroup.schedule(() -> null, 5, TimeUnit.NANOSECONDS); + try { + Uninterruptibles.getUninterruptibly(f, 100, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + fail("unexpected error", e.getCause()); + } catch (TimeoutException e) { + fail("timed out while waiting for admin tasks to complete", e); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/RemoveNodeRefreshTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/RemoveNodeRefreshTest.java new file mode 100644 index 00000000000..29053f2b08e --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/RemoveNodeRefreshTest.java @@ -0,0 +1,80 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class RemoveNodeRefreshTest { + + @Mock private InternalDriverContext context; + @Mock protected MetricsFactory metricsFactory; + + private DefaultNode node1; + private DefaultNode node2; + + @Before + public void setup() { + when(context.getMetricsFactory()).thenReturn(metricsFactory); + node1 = TestNodeFactory.newNode(1, context); + node2 = TestNodeFactory.newNode(2, context); + } + + @Test + public void should_remove_existing_node() { + // Given + DefaultMetadata oldMetadata = + new DefaultMetadata( + ImmutableMap.of(node1.getHostId(), node1, node2.getHostId(), node2), + Collections.emptyMap(), + null); + RemoveNodeRefresh refresh = new RemoveNodeRefresh(node2.getBroadcastRpcAddress().get()); + + // When + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + + // Then + assertThat(result.newMetadata.getNodes()).containsOnlyKeys(node1.getHostId()); + assertThat(result.events).containsExactly(NodeStateEvent.removed(node2)); + } + + @Test + public void should_not_remove_nonexistent_node() { + // Given + DefaultMetadata oldMetadata = + new DefaultMetadata( + ImmutableMap.of(node1.getHostId(), node1), Collections.emptyMap(), null); + RemoveNodeRefresh refresh = new RemoveNodeRefresh(node2.getBroadcastRpcAddress().get()); + + // When + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + + // Then + assertThat(result.newMetadata.getNodes()).containsOnlyKeys(node1.getHostId()); + assertThat(result.events).isEmpty(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/SchemaAgreementCheckerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/SchemaAgreementCheckerTest.java new file mode 100644 index 00000000000..0848ea3dcd9 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/SchemaAgreementCheckerTest.java @@ -0,0 +1,289 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterators; +import io.netty.channel.EventLoop; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Map; +import java.util.Queue; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SchemaAgreementCheckerTest { + + private static final UUID VERSION1 = UUID.randomUUID(); + private static final UUID VERSION2 = UUID.randomUUID(); + + @Mock private InternalDriverContext context; + @Mock private DriverConfig config; + @Mock private DriverExecutionProfile defaultConfig; + @Mock private DriverChannel channel; + @Mock private EventLoop eventLoop; + @Mock private MetadataManager metadataManager; + @Mock private MetricsFactory metricsFactory; + @Mock private Metadata metadata; + @Mock private DefaultNode node1; + @Mock private DefaultNode node2; + + @Before + public void setup() { + when(context.getMetricsFactory()).thenReturn(metricsFactory); + + node1 = TestNodeFactory.newNode(1, context); + node2 = TestNodeFactory.newNode(2, context); + + when(defaultConfig.getDuration(DefaultDriverOption.CONTROL_CONNECTION_TIMEOUT)) + .thenReturn(Duration.ofSeconds(1)); + when(defaultConfig.getDuration(DefaultDriverOption.CONTROL_CONNECTION_AGREEMENT_INTERVAL)) + .thenReturn(Duration.ofMillis(200)); + when(defaultConfig.getDuration(DefaultDriverOption.CONTROL_CONNECTION_AGREEMENT_TIMEOUT)) + .thenReturn(Duration.ofSeconds(10)); + when(defaultConfig.getBoolean(DefaultDriverOption.CONTROL_CONNECTION_AGREEMENT_WARN)) + .thenReturn(true); + when(config.getDefaultProfile()).thenReturn(defaultConfig); + when(context.getConfig()).thenReturn(config); + + Map nodes = ImmutableMap.of(node1.getHostId(), node1, node2.getHostId(), node2); + when(metadata.getNodes()).thenReturn(nodes); + when(metadataManager.getMetadata()).thenReturn(metadata); + when(context.getMetadataManager()).thenReturn(metadataManager); + + node2.state = NodeState.UP; + + when(eventLoop.schedule(any(Runnable.class), anyLong(), any(TimeUnit.class))) + .thenAnswer( + invocation -> { // Ignore delay and run immediately: + Runnable task = invocation.getArgument(0); + task.run(); + return null; + }); + when(channel.eventLoop()).thenReturn(eventLoop); + } + + @Test + public void should_skip_if_timeout_is_zero() { + // Given + when(defaultConfig.getDuration(DefaultDriverOption.CONTROL_CONNECTION_AGREEMENT_TIMEOUT)) + .thenReturn(Duration.ZERO); + TestSchemaAgreementChecker checker = new TestSchemaAgreementChecker(channel, context); + + // When + CompletionStage future = checker.run(); + + // Then + assertThatStage(future).isSuccess(b -> assertThat(b).isFalse()); + } + + @Test + public void should_succeed_if_only_one_node() { + // Given + TestSchemaAgreementChecker checker = new TestSchemaAgreementChecker(channel, context); + checker.stubQueries( + new StubbedQuery( + "SELECT schema_version FROM system.local WHERE key='local'", + mockResult(mockRow(null, VERSION1))), + new StubbedQuery( + "SELECT host_id, schema_version FROM system.peers", mockResult(/*empty*/ ))); + + // When + CompletionStage future = checker.run(); + + // Then + assertThatStage(future).isSuccess(b -> assertThat(b).isTrue()); + } + + @Test + public void should_succeed_if_versions_match_on_first_try() { + // Given + TestSchemaAgreementChecker checker = new TestSchemaAgreementChecker(channel, context); + checker.stubQueries( + new StubbedQuery( + "SELECT schema_version FROM system.local WHERE key='local'", + mockResult(mockRow(null, VERSION1))), + new StubbedQuery( + "SELECT host_id, schema_version FROM system.peers", + mockResult(mockRow(node2.getHostId(), VERSION1)))); + + // When + CompletionStage future = checker.run(); + + // Then + assertThatStage(future).isSuccess(b -> assertThat(b).isTrue()); + } + + @Test + public void should_ignore_down_peers() { + // Given + TestSchemaAgreementChecker checker = new TestSchemaAgreementChecker(channel, context); + node2.state = NodeState.DOWN; + checker.stubQueries( + new StubbedQuery( + "SELECT schema_version FROM system.local WHERE key='local'", + mockResult(mockRow(null, VERSION1))), + new StubbedQuery( + "SELECT host_id, schema_version FROM system.peers", + mockResult(mockRow(node2.getHostId(), VERSION2)))); + + // When + CompletionStage future = checker.run(); + + // Then + assertThatStage(future).isSuccess(b -> assertThat(b).isTrue()); + } + + @Test + public void should_ignore_malformed_rows() { + // Given + TestSchemaAgreementChecker checker = new TestSchemaAgreementChecker(channel, context); + checker.stubQueries( + new StubbedQuery( + "SELECT schema_version FROM system.local WHERE key='local'", + mockResult(mockRow(null, VERSION1))), + new StubbedQuery( + "SELECT host_id, schema_version FROM system.peers", + mockResult(mockRow(null, VERSION2)))); // missing host_id + + // When + CompletionStage future = checker.run(); + + // Then + assertThatStage(future).isSuccess(b -> assertThat(b).isTrue()); + } + + @Test + public void should_reschedule_if_versions_do_not_match_on_first_try() { + // Given + TestSchemaAgreementChecker checker = new TestSchemaAgreementChecker(channel, context); + checker.stubQueries( + // First round + new StubbedQuery( + "SELECT schema_version FROM system.local WHERE key='local'", + mockResult(mockRow(null, VERSION1))), + new StubbedQuery( + "SELECT host_id, schema_version FROM system.peers", + mockResult(mockRow(node2.getHostId(), VERSION2))), + + // Second round + new StubbedQuery( + "SELECT schema_version FROM system.local WHERE key='local'", + mockResult(mockRow(null, VERSION1))), + new StubbedQuery( + "SELECT host_id, schema_version FROM system.peers", + mockResult(mockRow(node2.getHostId(), VERSION1)))); + + // When + CompletionStage future = checker.run(); + + // Then + assertThatStage(future).isSuccess(b -> assertThat(b).isTrue()); + } + + @Test + public void should_fail_if_versions_do_not_match_after_timeout() { + // Given + when(defaultConfig.getDuration(DefaultDriverOption.CONTROL_CONNECTION_AGREEMENT_TIMEOUT)) + .thenReturn(Duration.ofNanos(10)); + TestSchemaAgreementChecker checker = new TestSchemaAgreementChecker(channel, context); + checker.stubQueries( + new StubbedQuery( + "SELECT schema_version FROM system.local WHERE key='local'", + mockResult(mockRow(null, VERSION1))), + new StubbedQuery( + "SELECT host_id, schema_version FROM system.peers", + mockResult(mockRow(node2.getHostId(), VERSION1)))); + + // When + CompletionStage future = checker.run(); + + // Then + assertThatStage(future).isSuccess(b -> assertThat(b).isFalse()); + } + + /** Extend to mock the query execution logic. */ + private static class TestSchemaAgreementChecker extends SchemaAgreementChecker { + + private final Queue queries = new ArrayDeque<>(); + + TestSchemaAgreementChecker(DriverChannel channel, InternalDriverContext context) { + super(channel, context, 9042, "test"); + } + + private void stubQueries(StubbedQuery... queries) { + this.queries.addAll(Arrays.asList(queries)); + } + + @Override + protected CompletionStage query(String queryString) { + StubbedQuery nextQuery = queries.poll(); + assertThat(nextQuery).isNotNull(); + assertThat(queryString).isEqualTo(nextQuery.queryString); + return CompletableFuture.completedFuture(nextQuery.result); + } + } + + private static class StubbedQuery { + private final String queryString; + private final AdminResult result; + + private StubbedQuery(String queryString, AdminResult result) { + this.queryString = queryString; + this.result = result; + } + } + + private AdminRow mockRow(UUID hostId, UUID schemaVersion) { + AdminRow row = mock(AdminRow.class); + when(row.getUuid("host_id")).thenReturn(hostId); + when(row.getUuid("schema_version")).thenReturn(schemaVersion); + return row; + } + + private AdminResult mockResult(AdminRow... rows) { + AdminResult result = mock(AdminResult.class); + when(result.iterator()).thenReturn(Iterators.forArray(rows)); + return result; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/TestNodeFactory.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/TestNodeFactory.java new file mode 100644 index 00000000000..3866bbf8ddb --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/TestNodeFactory.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata; + +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import java.net.InetSocketAddress; +import java.util.UUID; + +public class TestNodeFactory { + + public static DefaultNode newNode(int lastIpByte, InternalDriverContext context) { + DefaultEndPoint endPoint = newEndPoint(lastIpByte); + DefaultNode node = new DefaultNode(endPoint, context); + node.hostId = UUID.randomUUID(); + node.broadcastRpcAddress = endPoint.resolve(); + return node; + } + + public static DefaultEndPoint newEndPoint(int lastByteOfIp) { + return new DefaultEndPoint(new InetSocketAddress("127.0.0." + lastByteOfIp, 9042)); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/AggregateParserTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/AggregateParserTest.java new file mode 100644 index 00000000000..14dfe6bfb4e --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/AggregateParserTest.java @@ -0,0 +1,121 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.metadata.schema.AggregateMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.util.Collections; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; + +public class AggregateParserTest extends SchemaParserTestBase { + + private static final AdminRow SUM_AND_TO_STRING_ROW_2_2 = + mockAggregateRow( + "ks", + "sum_and_to_string", + ImmutableList.of("org.apache.cassandra.db.marshal.Int32Type"), + "plus", + "org.apache.cassandra.db.marshal.Int32Type", + "to_string", + "org.apache.cassandra.db.marshal.UTF8Type", + Bytes.fromHexString("0x00000000")); + + static final AdminRow SUM_AND_TO_STRING_ROW_3_0 = + mockAggregateRow( + "ks", + "sum_and_to_string", + ImmutableList.of("int"), + "plus", + "int", + "to_string", + "text", + "0"); + + @Before + public void setup() { + when(context.getCodecRegistry()).thenReturn(new DefaultCodecRegistry("test")); + when(context.getProtocolVersion()).thenReturn(ProtocolVersion.DEFAULT); + } + + @Test + public void should_parse_modern_table() { + AggregateParser parser = new AggregateParser(new DataTypeCqlNameParser(), context); + AggregateMetadata aggregate = + parser.parseAggregate(SUM_AND_TO_STRING_ROW_3_0, KEYSPACE_ID, Collections.emptyMap()); + + assertThat(aggregate.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(aggregate.getSignature().getName().asInternal()).isEqualTo("sum_and_to_string"); + assertThat(aggregate.getSignature().getParameterTypes()).containsExactly(DataTypes.INT); + + FunctionSignature stateFuncSignature = aggregate.getStateFuncSignature(); + assertThat(stateFuncSignature.getName().asInternal()).isEqualTo("plus"); + assertThat(stateFuncSignature.getParameterTypes()) + .containsExactly(DataTypes.INT, DataTypes.INT); + assertThat(aggregate.getStateType()).isEqualTo(DataTypes.INT); + + Optional finalFuncSignature = aggregate.getFinalFuncSignature(); + assertThat(finalFuncSignature).isPresent(); + assertThat(finalFuncSignature) + .hasValueSatisfying( + signature -> { + assertThat(signature.getName().asInternal()).isEqualTo("to_string"); + assertThat(signature.getParameterTypes()).containsExactly(DataTypes.INT); + }); + assertThat(aggregate.getReturnType()).isEqualTo(DataTypes.TEXT); + + assertThat(aggregate.getInitCond().get()).isInstanceOf(Integer.class).isEqualTo(0); + } + + @Test + public void should_parse_legacy_table() { + AggregateParser parser = new AggregateParser(new DataTypeClassNameParser(), context); + AggregateMetadata aggregate = + parser.parseAggregate(SUM_AND_TO_STRING_ROW_2_2, KEYSPACE_ID, Collections.emptyMap()); + + assertThat(aggregate.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(aggregate.getSignature().getName().asInternal()).isEqualTo("sum_and_to_string"); + assertThat(aggregate.getSignature().getParameterTypes()).containsExactly(DataTypes.INT); + + FunctionSignature stateFuncSignature = aggregate.getStateFuncSignature(); + assertThat(stateFuncSignature.getName().asInternal()).isEqualTo("plus"); + assertThat(stateFuncSignature.getParameterTypes()) + .containsExactly(DataTypes.INT, DataTypes.INT); + assertThat(aggregate.getStateType()).isEqualTo(DataTypes.INT); + + Optional finalFuncSignature = aggregate.getFinalFuncSignature(); + assertThat(finalFuncSignature).isPresent(); + assertThat(finalFuncSignature) + .hasValueSatisfying( + signature -> { + assertThat(signature.getName().asInternal()).isEqualTo("to_string"); + assertThat(signature.getParameterTypes()).containsExactly(DataTypes.INT); + }); + assertThat(aggregate.getReturnType()).isEqualTo(DataTypes.TEXT); + + assertThat(aggregate.getInitCond().get()).isInstanceOf(Integer.class).isEqualTo(0); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeClassNameParserTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeClassNameParserTest.java new file mode 100644 index 00000000000..21ff579464d --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeClassNameParserTest.java @@ -0,0 +1,181 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DataTypeClassNameParserTest { + + private static final CqlIdentifier KEYSPACE_ID = CqlIdentifier.fromInternal("ks"); + + @Mock private InternalDriverContext context; + private DataTypeClassNameParser parser; + + @Before + public void setUp() throws Exception { + parser = new DataTypeClassNameParser(); + } + + @Test + public void should_parse_native_types() { + for (Map.Entry entry : + DataTypeClassNameParser.NATIVE_TYPES_BY_CLASS_NAME.entrySet()) { + + String className = entry.getKey(); + DataType expectedType = entry.getValue(); + + assertThat(parse(className)).isEqualTo(expectedType); + } + } + + @Test + public void should_parse_collection_types() { + assertThat( + parse( + "org.apache.cassandra.db.marshal.ListType(" + + "org.apache.cassandra.db.marshal.UTF8Type)")) + .isEqualTo(DataTypes.listOf(DataTypes.TEXT)); + + assertThat( + parse( + "org.apache.cassandra.db.marshal.FrozenType(" + + ("org.apache.cassandra.db.marshal.ListType(" + + "org.apache.cassandra.db.marshal.UTF8Type))"))) + .isEqualTo(DataTypes.frozenListOf(DataTypes.TEXT)); + + assertThat( + parse( + "org.apache.cassandra.db.marshal.SetType(" + + "org.apache.cassandra.db.marshal.UTF8Type)")) + .isEqualTo(DataTypes.setOf(DataTypes.TEXT)); + + assertThat( + parse( + "org.apache.cassandra.db.marshal.MapType(" + + "org.apache.cassandra.db.marshal.UTF8Type," + + "org.apache.cassandra.db.marshal.UTF8Type)")) + .isEqualTo(DataTypes.mapOf(DataTypes.TEXT, DataTypes.TEXT)); + + assertThat( + parse( + "org.apache.cassandra.db.marshal.MapType(" + + "org.apache.cassandra.db.marshal.UTF8Type," + + "org.apache.cassandra.db.marshal.FrozenType(" + + ("org.apache.cassandra.db.marshal.MapType(" + + "org.apache.cassandra.db.marshal.Int32Type," + + "org.apache.cassandra.db.marshal.Int32Type)))"))) + .isEqualTo( + DataTypes.mapOf(DataTypes.TEXT, DataTypes.frozenMapOf(DataTypes.INT, DataTypes.INT))); + } + + @Test + public void should_parse_user_type_when_definition_not_already_available() { + UserDefinedType addressType = + (UserDefinedType) + parse( + "org.apache.cassandra.db.marshal.UserType(" + + "foo,61646472657373," + + ("737472656574:org.apache.cassandra.db.marshal.UTF8Type," + + "7a6970636f6465:org.apache.cassandra.db.marshal.Int32Type," + + ("70686f6e6573:org.apache.cassandra.db.marshal.SetType(" + + "org.apache.cassandra.db.marshal.UserType(foo,70686f6e65," + + "6e616d65:org.apache.cassandra.db.marshal.UTF8Type," + + "6e756d626572:org.apache.cassandra.db.marshal.UTF8Type)") + + "))")); + + assertThat(addressType.getKeyspace().asInternal()).isEqualTo("foo"); + assertThat(addressType.getName().asInternal()).isEqualTo("address"); + assertThat(addressType.isFrozen()).isTrue(); + assertThat(addressType.getFieldNames().size()).isEqualTo(3); + + assertThat(addressType.getFieldNames().get(0).asInternal()).isEqualTo("street"); + assertThat(addressType.getFieldTypes().get(0)).isEqualTo(DataTypes.TEXT); + + assertThat(addressType.getFieldNames().get(1).asInternal()).isEqualTo("zipcode"); + assertThat(addressType.getFieldTypes().get(1)).isEqualTo(DataTypes.INT); + + assertThat(addressType.getFieldNames().get(2).asInternal()).isEqualTo("phones"); + DataType phonesType = addressType.getFieldTypes().get(2); + assertThat(phonesType).isInstanceOf(SetType.class); + UserDefinedType phoneType = ((UserDefinedType) ((SetType) phonesType).getElementType()); + + assertThat(phoneType.getKeyspace().asInternal()).isEqualTo("foo"); + assertThat(phoneType.getName().asInternal()).isEqualTo("phone"); + assertThat(phoneType.isFrozen()).isTrue(); + assertThat(phoneType.getFieldNames().size()).isEqualTo(2); + + assertThat(phoneType.getFieldNames().get(0).asInternal()).isEqualTo("name"); + assertThat(phoneType.getFieldTypes().get(0)).isEqualTo(DataTypes.TEXT); + + assertThat(phoneType.getFieldNames().get(1).asInternal()).isEqualTo("number"); + assertThat(phoneType.getFieldTypes().get(1)).isEqualTo(DataTypes.TEXT); + } + + @Test + public void should_make_a_frozen_copy_user_type_when_definition_already_available() { + UserDefinedType existing = mock(UserDefinedType.class); + + parse( + "org.apache.cassandra.db.marshal.UserType(foo,70686f6e65," + + "6e616d65:org.apache.cassandra.db.marshal.UTF8Type," + + "6e756d626572:org.apache.cassandra.db.marshal.UTF8Type)", + ImmutableMap.of(CqlIdentifier.fromInternal("phone"), existing)); + + verify(existing).copy(true); + } + + @Test + public void should_parse_tuple() { + TupleType tupleType = + (TupleType) + parse( + "org.apache.cassandra.db.marshal.TupleType(" + + "org.apache.cassandra.db.marshal.Int32Type," + + "org.apache.cassandra.db.marshal.UTF8Type," + + "org.apache.cassandra.db.marshal.FloatType)"); + + assertThat(tupleType.getComponentTypes().size()).isEqualTo(3); + assertThat(tupleType.getComponentTypes().get(0)).isEqualTo(DataTypes.INT); + assertThat(tupleType.getComponentTypes().get(1)).isEqualTo(DataTypes.TEXT); + assertThat(tupleType.getComponentTypes().get(2)).isEqualTo(DataTypes.FLOAT); + } + + private DataType parse(String toParse) { + return parse(toParse, null); + } + + private DataType parse(String toParse, Map existingTypes) { + return parser.parse(KEYSPACE_ID, toParse, existingTypes, context); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeCqlNameParserTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeCqlNameParserTest.java new file mode 100644 index 00000000000..eff6e1290bb --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/DataTypeCqlNameParserTest.java @@ -0,0 +1,120 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.schema.ShallowUserDefinedType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class DataTypeCqlNameParserTest { + + private static final CqlIdentifier KEYSPACE_ID = CqlIdentifier.fromInternal("ks"); + + @Mock private InternalDriverContext context; + private DataTypeCqlNameParser parser; + + @Before + public void setUp() throws Exception { + parser = new DataTypeCqlNameParser(); + } + + @Test + public void should_parse_native_types() { + for (Map.Entry entry : + DataTypeCqlNameParser.NATIVE_TYPES_BY_NAME.entrySet()) { + + String className = entry.getKey(); + DataType expectedType = entry.getValue(); + + assertThat(parse(className)).isEqualTo(expectedType); + } + } + + @Test + public void should_parse_collection_types() { + assertThat(parse("list")).isEqualTo(DataTypes.listOf(DataTypes.TEXT)); + assertThat(parse("frozen>")).isEqualTo(DataTypes.frozenListOf(DataTypes.TEXT)); + assertThat(parse("set")).isEqualTo(DataTypes.setOf(DataTypes.TEXT)); + assertThat(parse("map")).isEqualTo(DataTypes.mapOf(DataTypes.TEXT, DataTypes.TEXT)); + assertThat(parse("map>>")) + .isEqualTo( + DataTypes.mapOf(DataTypes.TEXT, DataTypes.frozenMapOf(DataTypes.INT, DataTypes.INT))); + } + + @Test + public void should_parse_top_level_user_type_as_shallow() { + UserDefinedType addressType = (UserDefinedType) parse("address"); + assertThat(addressType).isInstanceOf(ShallowUserDefinedType.class); + assertThat(addressType.getKeyspace()).isEqualTo(KEYSPACE_ID); + assertThat(addressType.getName().asInternal()).isEqualTo("address"); + assertThat(addressType.isFrozen()).isFalse(); + + UserDefinedType frozenAddressType = (UserDefinedType) parse("frozen
"); + assertThat(frozenAddressType).isInstanceOf(ShallowUserDefinedType.class); + assertThat(frozenAddressType.getKeyspace()).isEqualTo(KEYSPACE_ID); + assertThat(frozenAddressType.getName().asInternal()).isEqualTo("address"); + assertThat(frozenAddressType.isFrozen()).isTrue(); + } + + @Test + public void should_reuse_existing_user_type_when_not_top_level() { + UserDefinedType addressType = mock(UserDefinedType.class); + UserDefinedType frozenAddressType = mock(UserDefinedType.class); + when(addressType.copy(false)).thenReturn(addressType); + when(addressType.copy(true)).thenReturn(frozenAddressType); + + ImmutableMap existingTypes = + ImmutableMap.of(CqlIdentifier.fromInternal("address"), addressType); + + ListType listOfAddress = (ListType) parse("list
", existingTypes); + assertThat(listOfAddress.getElementType()).isEqualTo(addressType); + + ListType listOfFrozenAddress = (ListType) parse("list>", existingTypes); + assertThat(listOfFrozenAddress.getElementType()).isEqualTo(frozenAddressType); + } + + @Test + public void should_parse_tuple() { + TupleType tupleType = (TupleType) parse("tuple"); + + assertThat(tupleType.getComponentTypes().size()).isEqualTo(3); + assertThat(tupleType.getComponentTypes().get(0)).isEqualTo(DataTypes.INT); + assertThat(tupleType.getComponentTypes().get(1)).isEqualTo(DataTypes.TEXT); + assertThat(tupleType.getComponentTypes().get(2)).isEqualTo(DataTypes.FLOAT); + } + + private DataType parse(String toParse) { + return parse(toParse, null); + } + + private DataType parse(String toParse, Map existingTypes) { + return parser.parse(KEYSPACE_ID, toParse, existingTypes, context); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/FunctionParserTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/FunctionParserTest.java new file mode 100644 index 00000000000..ba6928472ed --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/FunctionParserTest.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionMetadata; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.Collections; +import org.junit.Test; + +public class FunctionParserTest extends SchemaParserTestBase { + + private static final AdminRow ID_ROW_2_2 = + mockFunctionRow( + "ks", + "id", + ImmutableList.of("i"), + ImmutableList.of("org.apache.cassandra.db.marshal.Int32Type"), + "return i;", + false, + "java", + "org.apache.cassandra.db.marshal.Int32Type"); + + static final AdminRow ID_ROW_3_0 = + mockFunctionRow( + "ks", + "id", + ImmutableList.of("i"), + ImmutableList.of("int"), + "return i;", + false, + "java", + "int"); + + @Test + public void should_parse_modern_table() { + FunctionParser parser = new FunctionParser(new DataTypeCqlNameParser(), context); + FunctionMetadata function = + parser.parseFunction(ID_ROW_3_0, KEYSPACE_ID, Collections.emptyMap()); + + assertThat(function.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(function.getSignature().getName().asInternal()).isEqualTo("id"); + assertThat(function.getSignature().getParameterTypes()).containsExactly(DataTypes.INT); + assertThat(function.getParameterNames()).containsExactly(CqlIdentifier.fromInternal("i")); + assertThat(function.getBody()).isEqualTo("return i;"); + assertThat(function.isCalledOnNullInput()).isFalse(); + assertThat(function.getLanguage()).isEqualTo("java"); + assertThat(function.getReturnType()).isEqualTo(DataTypes.INT); + } + + @Test + public void should_parse_legacy_table() { + FunctionParser parser = new FunctionParser(new DataTypeClassNameParser(), context); + FunctionMetadata function = + parser.parseFunction(ID_ROW_2_2, KEYSPACE_ID, Collections.emptyMap()); + + assertThat(function.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(function.getSignature().getName().asInternal()).isEqualTo("id"); + assertThat(function.getSignature().getParameterTypes()).containsExactly(DataTypes.INT); + assertThat(function.getParameterNames()).containsExactly(CqlIdentifier.fromInternal("i")); + assertThat(function.getBody()).isEqualTo("return i;"); + assertThat(function.isCalledOnNullInput()).isFalse(); + assertThat(function.getLanguage()).isEqualTo("java"); + assertThat(function.getReturnType()).isEqualTo(DataTypes.INT); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParserTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParserTest.java new file mode 100644 index 00000000000..7e030628bf4 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParserTest.java @@ -0,0 +1,145 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.FunctionSignature; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.internal.core.metadata.MetadataRefresh; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.CassandraSchemaRows; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; +import com.datastax.oss.driver.internal.core.metadata.schema.refresh.SchemaRefresh; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.Map; +import java.util.function.Consumer; +import org.junit.Test; + +public class SchemaParserTest extends SchemaParserTestBase { + + @Test + public void should_parse_modern_keyspace_row() { + SchemaRefresh refresh = + (SchemaRefresh) + parse(rows -> rows.withKeyspaces(ImmutableList.of(mockModernKeyspaceRow("ks")))); + + assertThat(refresh.newKeyspaces).hasSize(1); + KeyspaceMetadata keyspace = refresh.newKeyspaces.values().iterator().next(); + checkKeyspace(keyspace); + } + + @Test + public void should_parse_legacy_keyspace_row() { + SchemaRefresh refresh = + (SchemaRefresh) + parse(rows -> rows.withKeyspaces(ImmutableList.of(mockLegacyKeyspaceRow("ks")))); + + assertThat(refresh.newKeyspaces).hasSize(1); + KeyspaceMetadata keyspace = refresh.newKeyspaces.values().iterator().next(); + checkKeyspace(keyspace); + } + + @Test + public void should_parse_keyspace_with_all_children() { + // Needed to parse the aggregate + when(context.getCodecRegistry()).thenReturn(new DefaultCodecRegistry("test")); + + SchemaRefresh refresh = + (SchemaRefresh) + parse( + rows -> + rows.withKeyspaces(ImmutableList.of(mockModernKeyspaceRow("ks"))) + .withTypes( + ImmutableList.of( + mockTypeRow( + "ks", "t", ImmutableList.of("i"), ImmutableList.of("int")))) + .withTables(ImmutableList.of(TableParserTest.TABLE_ROW_3_0)) + .withColumns(TableParserTest.COLUMN_ROWS_3_0) + .withIndexes(TableParserTest.INDEX_ROWS_3_0) + .withViews(ImmutableList.of(ViewParserTest.VIEW_ROW_3_0)) + .withColumns(ViewParserTest.COLUMN_ROWS_3_0) + .withFunctions(ImmutableList.of(FunctionParserTest.ID_ROW_3_0)) + .withAggregates( + ImmutableList.of(AggregateParserTest.SUM_AND_TO_STRING_ROW_3_0))); + + assertThat(refresh.newKeyspaces).hasSize(1); + KeyspaceMetadata keyspace = refresh.newKeyspaces.values().iterator().next(); + checkKeyspace(keyspace); + + assertThat(keyspace.getUserDefinedTypes()) + .hasSize(1) + .containsKey(CqlIdentifier.fromInternal("t")); + assertThat(keyspace.getTables()).hasSize(1).containsKey(CqlIdentifier.fromInternal("foo")); + assertThat(keyspace.getViews()) + .hasSize(1) + .containsKey(CqlIdentifier.fromInternal("alltimehigh")); + assertThat(keyspace.getFunctions()) + .hasSize(1) + .containsKey(new FunctionSignature(CqlIdentifier.fromInternal("id"), DataTypes.INT)); + assertThat(keyspace.getAggregates()) + .hasSize(1) + .containsKey( + new FunctionSignature(CqlIdentifier.fromInternal("sum_and_to_string"), DataTypes.INT)); + } + + // Common assertions, the keyspace has the same info in all of our single keyspace examples + private void checkKeyspace(KeyspaceMetadata keyspace) { + assertThat(keyspace.getName().asInternal()).isEqualTo("ks"); + assertThat(keyspace.isDurableWrites()).isTrue(); + assertThat(keyspace.getReplication()) + .hasSize(2) + .containsEntry("class", "org.apache.cassandra.locator.SimpleStrategy") + .containsEntry("replication_factor", "1"); + } + + @Test + public void should_parse_multiple_keyspaces() { + SchemaRefresh refresh = + (SchemaRefresh) + parse( + rows -> + rows.withKeyspaces( + ImmutableList.of( + mockModernKeyspaceRow("ks1"), mockModernKeyspaceRow("ks2"))) + .withTypes( + ImmutableList.of( + mockTypeRow( + "ks1", "t1", ImmutableList.of("i"), ImmutableList.of("int")), + mockTypeRow( + "ks2", "t2", ImmutableList.of("i"), ImmutableList.of("int"))))); + + Map keyspaces = refresh.newKeyspaces; + assertThat(keyspaces).hasSize(2); + KeyspaceMetadata ks1 = keyspaces.get(CqlIdentifier.fromInternal("ks1")); + KeyspaceMetadata ks2 = keyspaces.get(CqlIdentifier.fromInternal("ks2")); + + assertThat(ks1.getName().asInternal()).isEqualTo("ks1"); + assertThat(ks1.getUserDefinedTypes()).hasSize(1).containsKey(CqlIdentifier.fromInternal("t1")); + assertThat(ks2.getName().asInternal()).isEqualTo("ks2"); + assertThat(ks2.getUserDefinedTypes()).hasSize(1).containsKey(CqlIdentifier.fromInternal("t2")); + } + + private MetadataRefresh parse(Consumer builderConfig) { + CassandraSchemaRows.Builder builder = new CassandraSchemaRows.Builder(true, null, "test"); + builderConfig.accept(builder); + SchemaRows rows = builder.build(); + return new CassandraSchemaParser(rows, context).parse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParserTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParserTestBase.java new file mode 100644 index 00000000000..9adce5643d9 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/SchemaParserTestBase.java @@ -0,0 +1,294 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultMetadata; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import java.nio.ByteBuffer; +import java.util.List; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.Silent.class) +public abstract class SchemaParserTestBase { + + protected static final CqlIdentifier KEYSPACE_ID = CqlIdentifier.fromInternal("ks"); + @Mock protected DefaultMetadata currentMetadata; + @Mock protected InternalDriverContext context; + + protected static AdminRow mockFunctionRow( + String keyspace, + String name, + List argumentNames, + List argumentTypes, + String body, + boolean calledOnNullInput, + String language, + String returnType) { + + AdminRow row = mock(AdminRow.class); + + when(row.contains("keyspace_name")).thenReturn(true); + when(row.contains("function_name")).thenReturn(true); + when(row.contains("argument_names")).thenReturn(true); + when(row.contains("argument_types")).thenReturn(true); + when(row.contains("body")).thenReturn(true); + when(row.contains("called_on_null_input")).thenReturn(true); + when(row.contains("language")).thenReturn(true); + when(row.contains("return_type")).thenReturn(true); + + when(row.getString("keyspace_name")).thenReturn(keyspace); + when(row.getString("function_name")).thenReturn(name); + when(row.getListOfString("argument_names")).thenReturn(argumentNames); + when(row.getListOfString("argument_types")).thenReturn(argumentTypes); + when(row.getString("body")).thenReturn(body); + when(row.getBoolean("called_on_null_input")).thenReturn(calledOnNullInput); + when(row.getString("language")).thenReturn(language); + when(row.getString("return_type")).thenReturn(returnType); + + return row; + } + + protected static AdminRow mockAggregateRow( + String keyspace, + String name, + List argumentTypes, + String stateFunc, + String stateType, + String finalFunc, + String returnType, + Object initCond) { + + AdminRow row = mock(AdminRow.class); + + when(row.contains("keyspace_name")).thenReturn(true); + when(row.contains("aggregate_name")).thenReturn(true); + when(row.contains("argument_types")).thenReturn(true); + when(row.contains("state_func")).thenReturn(true); + when(row.contains("state_type")).thenReturn(true); + when(row.contains("final_func")).thenReturn(true); + when(row.contains("return_type")).thenReturn(true); + when(row.contains("initcond")).thenReturn(true); + + when(row.getString("keyspace_name")).thenReturn(keyspace); + when(row.getString("aggregate_name")).thenReturn(name); + when(row.getListOfString("argument_types")).thenReturn(argumentTypes); + when(row.getString("state_func")).thenReturn(stateFunc); + when(row.getString("state_type")).thenReturn(stateType); + when(row.getString("final_func")).thenReturn(finalFunc); + when(row.getString("return_type")).thenReturn(returnType); + + if (initCond instanceof ByteBuffer) { + when(row.isString("initcond")).thenReturn(false); + when(row.getByteBuffer("initcond")).thenReturn(((ByteBuffer) initCond)); + } else if (initCond instanceof String) { + when(row.isString("initcond")).thenReturn(true); + when(row.getString("initcond")).thenReturn(((String) initCond)); + } else { + fail("Unsupported initcond type" + initCond.getClass()); + } + + return row; + } + + protected static AdminRow mockTypeRow( + String keyspace, String name, List fieldNames, List fieldTypes) { + AdminRow row = mock(AdminRow.class); + + when(row.getString("keyspace_name")).thenReturn(keyspace); + when(row.getString("type_name")).thenReturn(name); + when(row.getListOfString("field_names")).thenReturn(fieldNames); + when(row.getListOfString("field_types")).thenReturn(fieldTypes); + + return row; + } + + protected static AdminRow mockLegacyTableRow(String keyspace, String name, String comparator) { + AdminRow row = mock(AdminRow.class); + + when(row.contains("table_name")).thenReturn(false); + + when(row.getString("keyspace_name")).thenReturn(keyspace); + when(row.getString("columnfamily_name")).thenReturn(name); + when(row.getBoolean("is_dense")).thenReturn(false); + when(row.getString("comparator")).thenReturn(comparator); + when(row.isString("caching")).thenReturn(true); + when(row.getString("caching")) + .thenReturn("{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}"); + when(row.getString("compaction_strategy_class")) + .thenReturn("org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy"); + when(row.getString("compaction_strategy_options")).thenReturn("{\"mock_option\":\"1\"}"); + + return row; + } + + protected static AdminRow mockLegacyColumnRow( + String keyspaceName, + String tableName, + String name, + String kind, + String dataType, + Integer position) { + return mockLegacyColumnRow( + keyspaceName, tableName, name, kind, dataType, position, null, null, null); + } + + protected static AdminRow mockLegacyColumnRow( + String keyspaceName, + String tableName, + String name, + String kind, + String dataType, + int position, + String indexName, + String indexType, + String indexOptions) { + AdminRow row = mock(AdminRow.class); + + when(row.contains("validator")).thenReturn(true); + + when(row.getString("keyspace_name")).thenReturn(keyspaceName); + when(row.getString("columnfamily_name")).thenReturn(tableName); + when(row.getString("column_name")).thenReturn(name); + when(row.getString("type")).thenReturn(kind); + when(row.getString("validator")).thenReturn(dataType); + when(row.getInteger("component_index")).thenReturn(position); + when(row.getString("index_name")).thenReturn(indexName); + when(row.getString("index_type")).thenReturn(indexType); + when(row.getString("index_options")).thenReturn(indexOptions); + + return row; + } + + protected static AdminRow mockModernTableRow(String keyspace, String name) { + AdminRow row = mock(AdminRow.class); + + when(row.contains("flags")).thenReturn(true); + when(row.contains("table_name")).thenReturn(true); + + when(row.getString("keyspace_name")).thenReturn(keyspace); + when(row.getString("table_name")).thenReturn(name); + when(row.getSetOfString("flags")).thenReturn(ImmutableSet.of("compound")); + when(row.isString("caching")).thenReturn(false); + when(row.get("caching", RelationParser.MAP_OF_TEXT_TO_TEXT)) + .thenReturn(ImmutableMap.of("keys", "ALL", "rows_per_partition", "NONE")); + when(row.get("compaction", RelationParser.MAP_OF_TEXT_TO_TEXT)) + .thenReturn( + ImmutableMap.of( + "class", + "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy", + "mock_option", + "1")); + + return row; + } + + protected static AdminRow mockModernColumnRow( + String keyspaceName, + String tableName, + String name, + String kind, + String dataType, + String clusteringOrder, + Integer position) { + AdminRow row = mock(AdminRow.class); + + when(row.contains("kind")).thenReturn(true); + when(row.contains("position")).thenReturn(true); + when(row.contains("clustering_order")).thenReturn(true); + + when(row.getString("keyspace_name")).thenReturn(keyspaceName); + when(row.getString("table_name")).thenReturn(tableName); + when(row.getString("column_name")).thenReturn(name); + when(row.getString("kind")).thenReturn(kind); + when(row.getString("type")).thenReturn(dataType); + when(row.getInteger("position")).thenReturn(position); + when(row.getString("clustering_order")).thenReturn(clusteringOrder); + + return row; + } + + protected static AdminRow mockIndexRow( + String keyspaceName, + String tableName, + String name, + String kind, + ImmutableMap options) { + AdminRow row = mock(AdminRow.class); + + when(row.getString("keyspace_name")).thenReturn(keyspaceName); + when(row.getString("table_name")).thenReturn(tableName); + when(row.getString("index_name")).thenReturn(name); + when(row.getString("kind")).thenReturn(kind); + when(row.getMapOfStringToString("options")).thenReturn(options); + + return row; + } + + protected static AdminRow mockViewRow( + String keyspaceName, + String viewName, + String baseTableName, + boolean includeAllColumns, + String whereClause) { + AdminRow row = mock(AdminRow.class); + + when(row.getString("keyspace_name")).thenReturn(keyspaceName); + when(row.getString("view_name")).thenReturn(viewName); + when(row.getString("base_table_name")).thenReturn(baseTableName); + when(row.getBoolean("include_all_columns")).thenReturn(includeAllColumns); + when(row.getString("where_clause")).thenReturn(whereClause); + + return row; + } + + protected static AdminRow mockModernKeyspaceRow(String keyspaceName) { + AdminRow row = mock(AdminRow.class); + + when(row.getString("keyspace_name")).thenReturn(keyspaceName); + when(row.getBoolean("durable_writes")).thenReturn(true); + + when(row.contains("strategy_class")).thenReturn(false); + when(row.getMapOfStringToString("replication")) + .thenReturn( + ImmutableMap.of( + "class", "org.apache.cassandra.locator.SimpleStrategy", "replication_factor", "1")); + + return row; + } + + protected static AdminRow mockLegacyKeyspaceRow(String keyspaceName) { + AdminRow row = mock(AdminRow.class); + + when(row.getString("keyspace_name")).thenReturn(keyspaceName); + when(row.getBoolean("durable_writes")).thenReturn(true); + + when(row.contains("strategy_class")).thenReturn(true); + when(row.getString("strategy_class")).thenReturn("org.apache.cassandra.locator.SimpleStrategy"); + when(row.getString("strategy_options")).thenReturn("{\"replication_factor\":\"1\"}"); + + return row; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/TableParserTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/TableParserTest.java new file mode 100644 index 00000000000..e361fb8a39d --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/TableParserTest.java @@ -0,0 +1,189 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.IndexKind; +import com.datastax.oss.driver.api.core.metadata.schema.IndexMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.TableMetadata; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.CassandraSchemaRows; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import org.junit.Test; + +public class TableParserTest extends SchemaParserTestBase { + + private static final AdminRow TABLE_ROW_2_2 = + mockLegacyTableRow( + "ks", + "foo", + "org.apache.cassandra.db.marshal.CompositeType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.UTF8Type)"); + private static final ImmutableList COLUMN_ROWS_2_2 = + ImmutableList.of( + mockLegacyColumnRow( + "ks", "foo", "k2", "partition_key", "org.apache.cassandra.db.marshal.UTF8Type", 1), + mockLegacyColumnRow( + "ks", "foo", "k1", "partition_key", "org.apache.cassandra.db.marshal.Int32Type", 0), + mockLegacyColumnRow( + "ks", "foo", "cc1", "clustering_key", "org.apache.cassandra.db.marshal.Int32Type", 0), + mockLegacyColumnRow( + "ks", + "foo", + "cc2", + "clustering_key", + "org.apache.cassandra.db.marshal.ReversedType(org.apache.cassandra.db.marshal.Int32Type)", + 1), + mockLegacyColumnRow( + "ks", + "foo", + "v", + "regular", + "org.apache.cassandra.db.marshal.ReversedType(org.apache.cassandra.db.marshal.Int32Type)", + -1, + "foo_v_idx", + "COMPOSITES", + "{}")); + + static final AdminRow TABLE_ROW_3_0 = mockModernTableRow("ks", "foo"); + static final ImmutableList COLUMN_ROWS_3_0 = + ImmutableList.of( + mockModernColumnRow("ks", "foo", "k2", "partition_key", "text", "none", 1), + mockModernColumnRow("ks", "foo", "k1", "partition_key", "int", "none", 0), + mockModernColumnRow("ks", "foo", "cc1", "clustering", "int", "asc", 0), + mockModernColumnRow("ks", "foo", "cc2", "clustering", "int", "desc", 1), + mockModernColumnRow("ks", "foo", "v", "regular", "int", "none", -1)); + static final ImmutableList INDEX_ROWS_3_0 = + ImmutableList.of( + mockIndexRow("ks", "foo", "foo_v_idx", "COMPOSITES", ImmutableMap.of("target", "v"))); + + @Test + public void should_skip_when_no_column_rows() { + SchemaRows rows = legacyRows(TABLE_ROW_2_2, Collections.emptyList()); + TableParser parser = new TableParser(rows, context); + TableMetadata table = parser.parseTable(TABLE_ROW_2_2, KEYSPACE_ID, Collections.emptyMap()); + + assertThat(table).isNull(); + } + + @Test + public void should_parse_legacy_tables() { + SchemaRows rows = legacyRows(TABLE_ROW_2_2, COLUMN_ROWS_2_2); + TableParser parser = new TableParser(rows, context); + TableMetadata table = parser.parseTable(TABLE_ROW_2_2, KEYSPACE_ID, Collections.emptyMap()); + + checkTable(table); + + assertThat(table.getOptions().get(CqlIdentifier.fromInternal("caching"))) + .isEqualTo("{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}"); + } + + @Test + public void should_parse_modern_tables() { + SchemaRows rows = modernRows(TABLE_ROW_3_0, COLUMN_ROWS_3_0, INDEX_ROWS_3_0); + TableParser parser = new TableParser(rows, context); + TableMetadata table = parser.parseTable(TABLE_ROW_3_0, KEYSPACE_ID, Collections.emptyMap()); + + checkTable(table); + + assertThat((Map) table.getOptions().get(CqlIdentifier.fromInternal("caching"))) + .hasSize(2) + .containsEntry("keys", "ALL") + .containsEntry("rows_per_partition", "NONE"); + } + + // Shared between 2.2 and 3.0 tests, all expected values are the same except the 'caching' option + private void checkTable(TableMetadata table) { + assertThat(table.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(table.getName().asInternal()).isEqualTo("foo"); + + assertThat(table.getPartitionKey()).hasSize(2); + ColumnMetadata pk0 = table.getPartitionKey().get(0); + assertThat(pk0.getName().asInternal()).isEqualTo("k1"); + assertThat(pk0.getType()).isEqualTo(DataTypes.INT); + ColumnMetadata pk1 = table.getPartitionKey().get(1); + assertThat(pk1.getName().asInternal()).isEqualTo("k2"); + assertThat(pk1.getType()).isEqualTo(DataTypes.TEXT); + + assertThat(table.getClusteringColumns().entrySet()).hasSize(2); + Iterator clusteringColumnsIterator = + table.getClusteringColumns().keySet().iterator(); + ColumnMetadata clusteringColumn1 = clusteringColumnsIterator.next(); + assertThat(clusteringColumn1.getName().asInternal()).isEqualTo("cc1"); + ColumnMetadata clusteringColumn2 = clusteringColumnsIterator.next(); + assertThat(clusteringColumn2.getName().asInternal()).isEqualTo("cc2"); + assertThat(table.getClusteringColumns().values()) + .containsExactly(ClusteringOrder.ASC, ClusteringOrder.DESC); + + assertThat(table.getColumns()) + .containsOnlyKeys( + CqlIdentifier.fromInternal("k1"), + CqlIdentifier.fromInternal("k2"), + CqlIdentifier.fromInternal("cc1"), + CqlIdentifier.fromInternal("cc2"), + CqlIdentifier.fromInternal("v")); + ColumnMetadata regularColumn = table.getColumns().get(CqlIdentifier.fromInternal("v")); + assertThat(regularColumn.getName().asInternal()).isEqualTo("v"); + assertThat(regularColumn.getType()).isEqualTo(DataTypes.INT); + + assertThat(table.getIndexes()).containsOnlyKeys(CqlIdentifier.fromInternal("foo_v_idx")); + IndexMetadata index = table.getIndexes().get(CqlIdentifier.fromInternal("foo_v_idx")); + assertThat(index.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(index.getTable().asInternal()).isEqualTo("foo"); + assertThat(index.getName().asInternal()).isEqualTo("foo_v_idx"); + assertThat(index.getClassName()).isNotPresent(); + assertThat(index.getKind()).isEqualTo(IndexKind.COMPOSITES); + assertThat(index.getTarget()).isEqualTo("v"); + assertThat( + (Map) table.getOptions().get(CqlIdentifier.fromInternal("compaction"))) + .hasSize(2) + .containsEntry("class", "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy") + .containsEntry("mock_option", "1"); + } + + private SchemaRows legacyRows(AdminRow tableRow, Iterable columnRows) { + return rows(tableRow, columnRows, null, false); + } + + private SchemaRows modernRows( + AdminRow tableRow, Iterable columnRows, Iterable indexesRows) { + return rows(tableRow, columnRows, indexesRows, true); + } + + private SchemaRows rows( + AdminRow tableRow, + Iterable columnRows, + Iterable indexesRows, + boolean isCassandraV3) { + CassandraSchemaRows.Builder builder = + new CassandraSchemaRows.Builder(isCassandraV3, null, "test") + .withTables(ImmutableList.of(tableRow)) + .withColumns(columnRows); + if (indexesRows != null) { + builder.withIndexes(indexesRows); + } + return builder.build(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/UserDefinedTypeListParserTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/UserDefinedTypeListParserTest.java new file mode 100644 index 00000000000..b77919d6dea --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/UserDefinedTypeListParserTest.java @@ -0,0 +1,227 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.MapType; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.Map; +import org.junit.Test; + +public class UserDefinedTypeListParserTest extends SchemaParserTestBase { + + private static final AdminRow PERSON_ROW_2_2 = + mockTypeRow( + "ks", + "person", + ImmutableList.of("first_name", "last_name", "address"), + ImmutableList.of( + "org.apache.cassandra.db.marshal.UTF8Type", + "org.apache.cassandra.db.marshal.UTF8Type", + "org.apache.cassandra.db.marshal.UserType(" + + "ks,61646472657373," // address + + "737472656574:org.apache.cassandra.db.marshal.UTF8Type," // street + + "7a6970636f6465:org.apache.cassandra.db.marshal.Int32Type)")); // zipcode + + private static final AdminRow PERSON_ROW_3_0 = + mockTypeRow( + "ks", + "person", + ImmutableList.of("first_name", "last_name", "address"), + ImmutableList.of("text", "text", "address")); + + private static final AdminRow ADDRESS_ROW_3_0 = + mockTypeRow( + "ks", "address", ImmutableList.of("street", "zipcode"), ImmutableList.of("text", "int")); + + @Test + public void should_parse_modern_table() { + UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context); + Map types = + parser.parse(KEYSPACE_ID, PERSON_ROW_3_0, ADDRESS_ROW_3_0); + + assertThat(types).hasSize(2); + UserDefinedType personType = types.get(CqlIdentifier.fromInternal("person")); + UserDefinedType addressType = types.get(CqlIdentifier.fromInternal("address")); + + assertThat(personType.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(personType.getName().asInternal()).isEqualTo("person"); + assertThat(personType.getFieldNames()) + .containsExactly( + CqlIdentifier.fromInternal("first_name"), + CqlIdentifier.fromInternal("last_name"), + CqlIdentifier.fromInternal("address")); + assertThat(personType.getFieldTypes().get(0)).isEqualTo(DataTypes.TEXT); + assertThat(personType.getFieldTypes().get(1)).isEqualTo(DataTypes.TEXT); + assertThat(personType.getFieldTypes().get(2)).isSameAs(addressType); + } + + @Test + public void should_parse_legacy_table() { + UserDefinedTypeParser parser = + new UserDefinedTypeParser(new DataTypeClassNameParser(), context); + // no need to add a column for the address type, because in 2.2 UDTs are always fully redefined + // in column and field types (instead of referencing an existing type) + Map types = parser.parse(KEYSPACE_ID, PERSON_ROW_2_2); + + assertThat(types).hasSize(1); + UserDefinedType personType = types.get(CqlIdentifier.fromInternal("person")); + + assertThat(personType.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(personType.getName().asInternal()).isEqualTo("person"); + assertThat(personType.getFieldNames()) + .containsExactly( + CqlIdentifier.fromInternal("first_name"), + CqlIdentifier.fromInternal("last_name"), + CqlIdentifier.fromInternal("address")); + assertThat(personType.getFieldTypes().get(0)).isEqualTo(DataTypes.TEXT); + assertThat(personType.getFieldTypes().get(1)).isEqualTo(DataTypes.TEXT); + UserDefinedType addressType = ((UserDefinedType) personType.getFieldTypes().get(2)); + assertThat(addressType.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(addressType.getName().asInternal()).isEqualTo("address"); + assertThat(addressType.getFieldNames()) + .containsExactly( + CqlIdentifier.fromInternal("street"), CqlIdentifier.fromInternal("zipcode")); + } + + @Test + public void should_parse_empty_list() { + UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context); + assertThat(parser.parse(KEYSPACE_ID /* no types*/)).isEmpty(); + } + + @Test + public void should_parse_singleton_list() { + UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context); + Map types = + parser.parse( + KEYSPACE_ID, mockTypeRow("ks", "t", ImmutableList.of("i"), ImmutableList.of("int"))); + + assertThat(types).hasSize(1); + UserDefinedType type = types.get(CqlIdentifier.fromInternal("t")); + assertThat(type.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(type.getName().asInternal()).isEqualTo("t"); + assertThat(type.getFieldNames()).containsExactly(CqlIdentifier.fromInternal("i")); + assertThat(type.getFieldTypes()).containsExactly(DataTypes.INT); + } + + @Test + public void should_resolve_list_dependency() { + UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context); + Map types = + parser.parse( + KEYSPACE_ID, + mockTypeRow( + "ks", "a", ImmutableList.of("bs"), ImmutableList.of("frozen>>")), + mockTypeRow("ks", "b", ImmutableList.of("i"), ImmutableList.of("int"))); + + assertThat(types).hasSize(2); + UserDefinedType aType = types.get(CqlIdentifier.fromInternal("a")); + UserDefinedType bType = types.get(CqlIdentifier.fromInternal("b")); + assertThat(((ListType) aType.getFieldTypes().get(0)).getElementType()).isEqualTo(bType); + } + + @Test + public void should_resolve_set_dependency() { + UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context); + Map types = + parser.parse( + KEYSPACE_ID, + mockTypeRow( + "ks", "a", ImmutableList.of("bs"), ImmutableList.of("frozen>>")), + mockTypeRow("ks", "b", ImmutableList.of("i"), ImmutableList.of("int"))); + + assertThat(types).hasSize(2); + UserDefinedType aType = types.get(CqlIdentifier.fromInternal("a")); + UserDefinedType bType = types.get(CqlIdentifier.fromInternal("b")); + assertThat(((SetType) aType.getFieldTypes().get(0)).getElementType()).isEqualTo(bType); + } + + @Test + public void should_resolve_map_dependency() { + UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context); + Map types = + parser.parse( + KEYSPACE_ID, + mockTypeRow( + "ks", + "a1", + ImmutableList.of("bs"), + ImmutableList.of("frozen>>")), + mockTypeRow( + "ks", + "a2", + ImmutableList.of("bs"), + ImmutableList.of("frozen, int>>")), + mockTypeRow("ks", "b", ImmutableList.of("i"), ImmutableList.of("int"))); + + assertThat(types).hasSize(3); + UserDefinedType a1Type = types.get(CqlIdentifier.fromInternal("a1")); + UserDefinedType a2Type = types.get(CqlIdentifier.fromInternal("a2")); + UserDefinedType bType = types.get(CqlIdentifier.fromInternal("b")); + assertThat(((MapType) a1Type.getFieldTypes().get(0)).getValueType()).isEqualTo(bType); + assertThat(((MapType) a2Type.getFieldTypes().get(0)).getKeyType()).isEqualTo(bType); + } + + @Test + public void should_resolve_tuple_dependency() { + UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context); + Map types = + parser.parse( + KEYSPACE_ID, + mockTypeRow( + "ks", + "a", + ImmutableList.of("b"), + ImmutableList.of("frozen>>")), + mockTypeRow("ks", "b", ImmutableList.of("i"), ImmutableList.of("int"))); + + assertThat(types).hasSize(2); + UserDefinedType aType = types.get(CqlIdentifier.fromInternal("a")); + UserDefinedType bType = types.get(CqlIdentifier.fromInternal("b")); + assertThat(((TupleType) aType.getFieldTypes().get(0)).getComponentTypes().get(1)) + .isEqualTo(bType); + } + + @Test + public void should_resolve_nested_dependency() { + UserDefinedTypeParser parser = new UserDefinedTypeParser(new DataTypeCqlNameParser(), context); + Map types = + parser.parse( + KEYSPACE_ID, + mockTypeRow( + "ks", + "a", + ImmutableList.of("bs"), + ImmutableList.of("frozen>>>>")), + mockTypeRow("ks", "b", ImmutableList.of("i"), ImmutableList.of("int"))); + + assertThat(types).hasSize(2); + UserDefinedType aType = types.get(CqlIdentifier.fromInternal("a")); + UserDefinedType bType = types.get(CqlIdentifier.fromInternal("b")); + TupleType tupleType = (TupleType) aType.getFieldTypes().get(0); + ListType listType = (ListType) tupleType.getComponentTypes().get(1); + assertThat(listType.getElementType()).isEqualTo(bType); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/ViewParserTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/ViewParserTest.java new file mode 100644 index 00000000000..6ba458bebfb --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/parsing/ViewParserTest.java @@ -0,0 +1,94 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.parsing; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.ColumnMetadata; +import com.datastax.oss.driver.api.core.metadata.schema.ViewMetadata; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.CassandraSchemaRows; +import com.datastax.oss.driver.internal.core.metadata.schema.queries.SchemaRows; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.Collections; +import java.util.Iterator; +import org.junit.Test; + +public class ViewParserTest extends SchemaParserTestBase { + + static final AdminRow VIEW_ROW_3_0 = + mockViewRow("ks", "alltimehigh", "scores", false, "game IS NOT NULL"); + static final ImmutableList COLUMN_ROWS_3_0 = + ImmutableList.of( + mockModernColumnRow("ks", "alltimehigh", "game", "partition_key", "text", "none", 0), + mockModernColumnRow("ks", "alltimehigh", "score", "clustering", "int", "desc", 0), + mockModernColumnRow("ks", "alltimehigh", "user", "clustering", "text", "asc", 1), + mockModernColumnRow("ks", "alltimehigh", "year", "clustering", "int", "asc", 2), + mockModernColumnRow("ks", "alltimehigh", "month", "clustering", "int", "asc", 3), + mockModernColumnRow("ks", "alltimehigh", "day", "clustering", "int", "asc", 4)); + + @Test + public void should_skip_when_no_column_rows() { + SchemaRows rows = rows(VIEW_ROW_3_0, Collections.emptyList()); + ViewParser parser = new ViewParser(rows, context); + ViewMetadata view = parser.parseView(VIEW_ROW_3_0, KEYSPACE_ID, Collections.emptyMap()); + + assertThat(view).isNull(); + } + + @Test + public void should_parse_view() { + SchemaRows rows = rows(VIEW_ROW_3_0, COLUMN_ROWS_3_0); + ViewParser parser = new ViewParser(rows, context); + ViewMetadata view = parser.parseView(VIEW_ROW_3_0, KEYSPACE_ID, Collections.emptyMap()); + + assertThat(view.getKeyspace().asInternal()).isEqualTo("ks"); + assertThat(view.getName().asInternal()).isEqualTo("alltimehigh"); + assertThat(view.getBaseTable().asInternal()).isEqualTo("scores"); + + assertThat(view.getPartitionKey()).hasSize(1); + ColumnMetadata pk0 = view.getPartitionKey().get(0); + assertThat(pk0.getName().asInternal()).isEqualTo("game"); + assertThat(pk0.getType()).isEqualTo(DataTypes.TEXT); + + assertThat(view.getClusteringColumns().entrySet()).hasSize(5); + Iterator clusteringColumnsIterator = + view.getClusteringColumns().keySet().iterator(); + assertThat(clusteringColumnsIterator.next().getName().asInternal()).isEqualTo("score"); + assertThat(clusteringColumnsIterator.next().getName().asInternal()).isEqualTo("user"); + assertThat(clusteringColumnsIterator.next().getName().asInternal()).isEqualTo("year"); + assertThat(clusteringColumnsIterator.next().getName().asInternal()).isEqualTo("month"); + assertThat(clusteringColumnsIterator.next().getName().asInternal()).isEqualTo("day"); + + assertThat(view.getColumns()) + .containsOnlyKeys( + CqlIdentifier.fromInternal("game"), + CqlIdentifier.fromInternal("score"), + CqlIdentifier.fromInternal("user"), + CqlIdentifier.fromInternal("year"), + CqlIdentifier.fromInternal("month"), + CqlIdentifier.fromInternal("day")); + } + + private SchemaRows rows(AdminRow viewRow, Iterable columnRows) { + return new CassandraSchemaRows.Builder(true, null, "test") + .withViews(ImmutableList.of(viewRow)) + .withColumns(columnRows) + .build(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra21SchemaQueriesTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra21SchemaQueriesTest.java new file mode 100644 index 00000000000..9fbfa0e7349 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra21SchemaQueriesTest.java @@ -0,0 +1,133 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import java.util.Collections; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.LinkedBlockingDeque; +import org.junit.Test; + +// Note: we don't repeat the other tests in Cassandra3SchemaQueriesTest because the logic is +// shared, this class just validates the query strings. +public class Cassandra21SchemaQueriesTest extends SchemaQueriesTest { + + @Test + public void should_query() { + when(config.getStringList( + DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, Collections.emptyList())) + .thenReturn(Collections.emptyList()); + + SchemaQueriesWithMockedChannel queries = + new SchemaQueriesWithMockedChannel(driverChannel, null, config, "test"); + + CompletionStage result = queries.execute(); + + // Keyspace + Call call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system.schema_keyspaces"); + call.result.complete( + mockResult(mockRow("keyspace_name", "ks1"), mockRow("keyspace_name", "ks2"))); + + // Types + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system.schema_usertypes"); + call.result.complete(mockResult(mockRow("keyspace_name", "ks1", "type_name", "type"))); + + // Tables + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system.schema_columnfamilies"); + call.result.complete(mockResult(mockRow("keyspace_name", "ks1", "columnfamily_name", "foo"))); + + // Columns + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system.schema_columns"); + call.result.complete( + mockResult( + mockRow("keyspace_name", "ks1", "columnfamily_name", "foo", "column_name", "k"))); + + channel.runPendingTasks(); + + assertThatStage(result) + .isSuccess( + rows -> { + // Keyspace + assertThat(rows.keyspaces()).hasSize(2); + assertThat(rows.keyspaces().get(0).getString("keyspace_name")).isEqualTo("ks1"); + assertThat(rows.keyspaces().get(1).getString("keyspace_name")).isEqualTo("ks2"); + + // Types + assertThat(rows.types().keySet()).containsOnly(KS1_ID); + assertThat(rows.types().get(KS1_ID)).hasSize(1); + assertThat(rows.types().get(KS1_ID).iterator().next().getString("type_name")) + .isEqualTo("type"); + + // Tables + assertThat(rows.tables().keySet()).containsOnly(KS1_ID); + assertThat(rows.tables().get(KS1_ID)).hasSize(1); + assertThat(rows.tables().get(KS1_ID).iterator().next().getString("columnfamily_name")) + .isEqualTo("foo"); + + // Rows + assertThat(rows.columns().keySet()).containsOnly(KS1_ID); + assertThat(rows.columns().get(KS1_ID).keySet()).containsOnly(FOO_ID); + assertThat( + rows.columns() + .get(KS1_ID) + .get(FOO_ID) + .iterator() + .next() + .getString("column_name")) + .isEqualTo("k"); + + // No views, functions or aggregates in this version + assertThat(rows.views().keySet()).isEmpty(); + assertThat(rows.functions().keySet()).isEmpty(); + assertThat(rows.aggregates().keySet()).isEmpty(); + }); + } + + /** Extends the class under test to mock the query execution logic. */ + static class SchemaQueriesWithMockedChannel extends Cassandra21SchemaQueries { + + final Queue calls = new LinkedBlockingDeque<>(); + + SchemaQueriesWithMockedChannel( + DriverChannel channel, + CompletableFuture refreshFuture, + DriverExecutionProfile config, + String logPrefix) { + super(channel, refreshFuture, config, logPrefix); + } + + @Override + protected CompletionStage query(String query) { + Call call = new Call(query); + calls.add(call); + return call.result; + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra22SchemaQueriesTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra22SchemaQueriesTest.java new file mode 100644 index 00000000000..7fd37d2541a --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra22SchemaQueriesTest.java @@ -0,0 +1,154 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import java.util.Collections; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.LinkedBlockingDeque; +import org.junit.Test; + +// Note: we don't repeat the other tests in Cassandra3SchemaQueriesTest because the logic is +// shared, this class just validates the query strings. +public class Cassandra22SchemaQueriesTest extends SchemaQueriesTest { + + @Test + public void should_query() { + when(config.getStringList( + DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, Collections.emptyList())) + .thenReturn(Collections.emptyList()); + + SchemaQueriesWithMockedChannel queries = + new SchemaQueriesWithMockedChannel(driverChannel, null, config, "test"); + + CompletionStage result = queries.execute(); + + // Keyspace + Call call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system.schema_keyspaces"); + call.result.complete( + mockResult(mockRow("keyspace_name", "ks1"), mockRow("keyspace_name", "ks2"))); + + // Types + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system.schema_usertypes"); + call.result.complete(mockResult(mockRow("keyspace_name", "ks1", "type_name", "type"))); + + // Tables + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system.schema_columnfamilies"); + call.result.complete(mockResult(mockRow("keyspace_name", "ks1", "columnfamily_name", "foo"))); + + // Columns + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system.schema_columns"); + call.result.complete( + mockResult( + mockRow("keyspace_name", "ks1", "columnfamily_name", "foo", "column_name", "k"))); + + // Functions + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system.schema_functions"); + call.result.complete(mockResult(mockRow("keyspace_name", "ks2", "function_name", "add"))); + + // Aggregates + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system.schema_aggregates"); + call.result.complete(mockResult(mockRow("keyspace_name", "ks2", "aggregate_name", "add"))); + + channel.runPendingTasks(); + + assertThatStage(result) + .isSuccess( + rows -> { + // Keyspace + assertThat(rows.keyspaces()).hasSize(2); + assertThat(rows.keyspaces().get(0).getString("keyspace_name")).isEqualTo("ks1"); + assertThat(rows.keyspaces().get(1).getString("keyspace_name")).isEqualTo("ks2"); + + // Types + assertThat(rows.types().keySet()).containsOnly(KS1_ID); + assertThat(rows.types().get(KS1_ID)).hasSize(1); + assertThat(rows.types().get(KS1_ID).iterator().next().getString("type_name")) + .isEqualTo("type"); + + // Tables + assertThat(rows.tables().keySet()).containsOnly(KS1_ID); + assertThat(rows.tables().get(KS1_ID)).hasSize(1); + assertThat(rows.tables().get(KS1_ID).iterator().next().getString("columnfamily_name")) + .isEqualTo("foo"); + + // Rows + assertThat(rows.columns().keySet()).containsOnly(KS1_ID); + assertThat(rows.columns().get(KS1_ID).keySet()).containsOnly(FOO_ID); + assertThat( + rows.columns() + .get(KS1_ID) + .get(FOO_ID) + .iterator() + .next() + .getString("column_name")) + .isEqualTo("k"); + + // Functions + assertThat(rows.functions().keySet()).containsOnly(KS2_ID); + assertThat(rows.functions().get(KS2_ID)).hasSize(1); + assertThat(rows.functions().get(KS2_ID).iterator().next().getString("function_name")) + .isEqualTo("add"); + + // Aggregates + assertThat(rows.aggregates().keySet()).containsOnly(KS2_ID); + assertThat(rows.aggregates().get(KS2_ID)).hasSize(1); + assertThat( + rows.aggregates().get(KS2_ID).iterator().next().getString("aggregate_name")) + .isEqualTo("add"); + + // No views in this version + assertThat(rows.views().keySet()).isEmpty(); + }); + } + + /** Extends the class under test to mock the query execution logic. */ + static class SchemaQueriesWithMockedChannel extends Cassandra22SchemaQueries { + + final Queue calls = new LinkedBlockingDeque<>(); + + SchemaQueriesWithMockedChannel( + DriverChannel channel, + CompletableFuture refreshFuture, + DriverExecutionProfile config, + String logPrefix) { + super(channel, refreshFuture, config, logPrefix); + } + + @Override + protected CompletionStage query(String query) { + Call call = new Call(query); + calls.add(call); + return call.result; + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra3SchemaQueriesTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra3SchemaQueriesTest.java new file mode 100644 index 00000000000..e2792935378 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/Cassandra3SchemaQueriesTest.java @@ -0,0 +1,346 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.Collections; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.LinkedBlockingDeque; +import org.junit.Before; +import org.junit.Test; + +public class Cassandra3SchemaQueriesTest extends SchemaQueriesTest { + + @Before + @Override + public void setup() { + super.setup(); + + // By default, no keyspace filter + when(config.getStringList( + DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, Collections.emptyList())) + .thenReturn(Collections.emptyList()); + } + + @Test + public void should_query_without_keyspace_filter() { + should_query_with_where_clause(""); + } + + @Test + public void should_query_with_keyspace_filter() { + when(config.getStringList( + DefaultDriverOption.METADATA_SCHEMA_REFRESHED_KEYSPACES, Collections.emptyList())) + .thenReturn(ImmutableList.of("ks1", "ks2")); + + should_query_with_where_clause(" WHERE keyspace_name in ('ks1','ks2')"); + } + + private void should_query_with_where_clause(String whereClause) { + SchemaQueriesWithMockedChannel queries = + new SchemaQueriesWithMockedChannel(driverChannel, null, config, "test"); + CompletionStage result = queries.execute(); + + // Keyspace + Call call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.keyspaces" + whereClause); + call.result.complete( + mockResult(mockRow("keyspace_name", "ks1"), mockRow("keyspace_name", "ks2"))); + + // Types + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.types" + whereClause); + call.result.complete(mockResult(mockRow("keyspace_name", "ks1", "type_name", "type"))); + + // Tables + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.tables" + whereClause); + call.result.complete(mockResult(mockRow("keyspace_name", "ks1", "table_name", "foo"))); + + // Columns + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.columns" + whereClause); + call.result.complete( + mockResult(mockRow("keyspace_name", "ks1", "table_name", "foo", "column_name", "k"))); + + // Indexes + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.indexes" + whereClause); + call.result.complete( + mockResult(mockRow("keyspace_name", "ks1", "table_name", "foo", "index_name", "index"))); + + // Views + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.views" + whereClause); + call.result.complete(mockResult(mockRow("keyspace_name", "ks2", "view_name", "foo"))); + + // Functions + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.functions" + whereClause); + call.result.complete(mockResult(mockRow("keyspace_name", "ks2", "function_name", "add"))); + + // Aggregates + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.aggregates" + whereClause); + call.result.complete(mockResult(mockRow("keyspace_name", "ks2", "aggregate_name", "add"))); + + channel.runPendingTasks(); + + assertThatStage(result) + .isSuccess( + rows -> { + // Keyspace + assertThat(rows.keyspaces()).hasSize(2); + assertThat(rows.keyspaces().get(0).getString("keyspace_name")).isEqualTo("ks1"); + assertThat(rows.keyspaces().get(1).getString("keyspace_name")).isEqualTo("ks2"); + + // Types + assertThat(rows.types().keySet()).containsOnly(KS1_ID); + assertThat(rows.types().get(KS1_ID)).hasSize(1); + assertThat(rows.types().get(KS1_ID).iterator().next().getString("type_name")) + .isEqualTo("type"); + + // Tables + assertThat(rows.tables().keySet()).containsOnly(KS1_ID); + assertThat(rows.tables().get(KS1_ID)).hasSize(1); + assertThat(rows.tables().get(KS1_ID).iterator().next().getString("table_name")) + .isEqualTo("foo"); + + // Columns + assertThat(rows.columns().keySet()).containsOnly(KS1_ID); + assertThat(rows.columns().get(KS1_ID).keySet()).containsOnly(FOO_ID); + assertThat( + rows.columns() + .get(KS1_ID) + .get(FOO_ID) + .iterator() + .next() + .getString("column_name")) + .isEqualTo("k"); + + // Indexes + assertThat(rows.indexes().keySet()).containsOnly(KS1_ID); + assertThat(rows.indexes().get(KS1_ID).keySet()).containsOnly(FOO_ID); + assertThat( + rows.indexes() + .get(KS1_ID) + .get(FOO_ID) + .iterator() + .next() + .getString("index_name")) + .isEqualTo("index"); + + // Views + assertThat(rows.views().keySet()).containsOnly(KS2_ID); + assertThat(rows.views().get(KS2_ID)).hasSize(1); + assertThat(rows.views().get(KS2_ID).iterator().next().getString("view_name")) + .isEqualTo("foo"); + + // Functions + assertThat(rows.functions().keySet()).containsOnly(KS2_ID); + assertThat(rows.functions().get(KS2_ID)).hasSize(1); + assertThat(rows.functions().get(KS2_ID).iterator().next().getString("function_name")) + .isEqualTo("add"); + + // Aggregates + assertThat(rows.aggregates().keySet()).containsOnly(KS2_ID); + assertThat(rows.aggregates().get(KS2_ID)).hasSize(1); + assertThat( + rows.aggregates().get(KS2_ID).iterator().next().getString("aggregate_name")) + .isEqualTo("add"); + }); + } + + @Test + public void should_query_with_paging() { + SchemaQueriesWithMockedChannel queries = + new SchemaQueriesWithMockedChannel(driverChannel, null, config, "test"); + CompletionStage result = queries.execute(); + + // Keyspace + Call call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.keyspaces"); + call.result.complete(mockResult(mockRow("keyspace_name", "ks1"))); + + // No types + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.types"); + call.result.complete(mockResult(/*empty*/ )); + + // Tables + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.tables"); + call.result.complete(mockResult(mockRow("keyspace_name", "ks1", "table_name", "foo"))); + + // Columns: paged + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.columns"); + + AdminResult page2 = + mockResult(mockRow("keyspace_name", "ks1", "table_name", "foo", "column_name", "v")); + AdminResult page1 = + mockResult(page2, mockRow("keyspace_name", "ks1", "table_name", "foo", "column_name", "k")); + call.result.complete(page1); + + // No indexes + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.indexes"); + call.result.complete(mockResult(/*empty*/ )); + + // No views + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.views"); + call.result.complete(mockResult(/*empty*/ )); + + // No functions + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.functions"); + call.result.complete(mockResult(/*empty*/ )); + + // No aggregates + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.aggregates"); + call.result.complete(mockResult(/*empty*/ )); + + channel.runPendingTasks(); + + assertThatStage(result) + .isSuccess( + rows -> { + assertThat(rows.columns().keySet()).containsOnly(KS1_ID); + assertThat(rows.columns().get(KS1_ID).keySet()).containsOnly(FOO_ID); + assertThat(rows.columns().get(KS1_ID).get(FOO_ID)) + .extracting(r -> r.getString("column_name")) + .containsExactly("k", "v"); + }); + } + + @Test + public void should_ignore_malformed_rows() { + SchemaQueriesWithMockedChannel queries = + new SchemaQueriesWithMockedChannel(driverChannel, null, config, "test"); + CompletionStage result = queries.execute(); + + // Keyspace + Call call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.keyspaces"); + call.result.complete(mockResult(mockRow("keyspace_name", "ks1"))); + + // No types + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.types"); + call.result.complete(mockResult(/*empty*/ )); + + // Tables + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.tables"); + call.result.complete( + mockResult( + mockRow("keyspace_name", "ks", "table_name", "foo"), + // Missing keyspace name: + mockRow("table_name", "foo"))); + + // Columns + call = queries.calls.poll(); + call.result.complete( + mockResult( + mockRow("keyspace_name", "ks", "table_name", "foo", "column_name", "k"), + // Missing keyspace name: + mockRow("table_name", "foo", "column_name", "k"), + // Missing table name: + mockRow("keyspace_name", "ks", "column_name", "k"))); + + AdminResult page2 = + mockResult(mockRow("keyspace_name", "ks1", "table_name", "foo", "column_name", "v")); + AdminResult page1 = + mockResult(page2, mockRow("keyspace_name", "ks1", "table_name", "foo", "column_name", "k")); + call.result.complete(page1); + + // No indexes + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.indexes"); + call.result.complete(mockResult(/*empty*/ )); + + // No views + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.views"); + call.result.complete(mockResult(/*empty*/ )); + + // No functions + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.functions"); + call.result.complete(mockResult(/*empty*/ )); + + // No aggregates + call = queries.calls.poll(); + assertThat(call.query).isEqualTo("SELECT * FROM system_schema.aggregates"); + call.result.complete(mockResult(/*empty*/ )); + + channel.runPendingTasks(); + + assertThatStage(result) + .isSuccess( + rows -> { + assertThat(rows.tables().keySet()).containsOnly(KS_ID); + assertThat(rows.tables().get(KS_ID)).hasSize(1); + assertThat(rows.tables().get(KS_ID).iterator().next().getString("table_name")) + .isEqualTo("foo"); + + assertThat(rows.columns().keySet()).containsOnly(KS_ID); + assertThat(rows.columns().get(KS_ID).keySet()).containsOnly(FOO_ID); + assertThat( + rows.columns() + .get(KS_ID) + .get(FOO_ID) + .iterator() + .next() + .getString("column_name")) + .isEqualTo("k"); + }); + } + + /** Extends the class under test to mock the query execution logic. */ + static class SchemaQueriesWithMockedChannel extends Cassandra3SchemaQueries { + + final Queue calls = new LinkedBlockingDeque<>(); + + SchemaQueriesWithMockedChannel( + DriverChannel channel, + CompletableFuture refreshFuture, + DriverExecutionProfile config, + String logPrefix) { + super(channel, refreshFuture, config, logPrefix); + } + + @Override + protected CompletionStage query(String query) { + Call call = new Call(query); + calls.add(call); + return call.result; + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaQueriesTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaQueriesTest.java new file mode 100644 index 00000000000..dd309ffac37 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/queries/SchemaQueriesTest.java @@ -0,0 +1,97 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.queries; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.adminrequest.AdminRow; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterators; +import io.netty.channel.embedded.EmbeddedChannel; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public abstract class SchemaQueriesTest { + + protected static final CqlIdentifier KS_ID = CqlIdentifier.fromInternal("ks"); + protected static final CqlIdentifier KS1_ID = CqlIdentifier.fromInternal("ks1"); + protected static final CqlIdentifier KS2_ID = CqlIdentifier.fromInternal("ks2"); + protected static final CqlIdentifier FOO_ID = CqlIdentifier.fromInternal("foo"); + + @Mock protected Node node; + @Mock protected DriverExecutionProfile config; + @Mock protected DriverChannel driverChannel; + protected EmbeddedChannel channel; + + @Before + public void setup() { + // Whatever, not actually used because the requests are mocked + when(config.getDuration(DefaultDriverOption.METADATA_SCHEMA_REQUEST_TIMEOUT)) + .thenReturn(Duration.ZERO); + when(config.getInt(DefaultDriverOption.METADATA_SCHEMA_REQUEST_PAGE_SIZE)).thenReturn(5000); + + channel = new EmbeddedChannel(); + driverChannel = mock(DriverChannel.class); + when(driverChannel.eventLoop()).thenReturn(channel.eventLoop()); + } + + protected static AdminRow mockRow(String... values) { + AdminRow row = mock(AdminRow.class); + assertThat(values.length % 2).as("Expecting an even number of parameters").isZero(); + for (int i = 0; i < values.length / 2; i++) { + when(row.getString(values[i * 2])).thenReturn(values[i * 2 + 1]); + } + return row; + } + + protected static AdminResult mockResult(AdminRow... rows) { + return mockResult(null, rows); + } + + protected static AdminResult mockResult(AdminResult next, AdminRow... rows) { + AdminResult result = mock(AdminResult.class); + if (next == null) { + when(result.hasNextPage()).thenReturn(false); + } else { + when(result.hasNextPage()).thenReturn(true); + when(result.nextPage()).thenReturn(CompletableFuture.completedFuture(next)); + } + when(result.iterator()).thenReturn(Iterators.forArray(rows)); + return result; + } + + protected static class Call { + final String query; + final CompletableFuture result; + + Call(String query) { + this.query = query; + this.result = new CompletableFuture<>(); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/refresh/SchemaRefreshTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/refresh/SchemaRefreshTest.java new file mode 100644 index 00000000000..1f171d90611 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/schema/refresh/SchemaRefreshTest.java @@ -0,0 +1,160 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.schema.refresh; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultMetadata; +import com.datastax.oss.driver.internal.core.metadata.MetadataRefresh; +import com.datastax.oss.driver.internal.core.metadata.schema.DefaultKeyspaceMetadata; +import com.datastax.oss.driver.internal.core.metadata.schema.events.KeyspaceChangeEvent; +import com.datastax.oss.driver.internal.core.metadata.schema.events.TypeChangeEvent; +import com.datastax.oss.driver.internal.core.type.UserDefinedTypeBuilder; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SchemaRefreshTest { + + private static final UserDefinedType OLD_T1 = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks1"), CqlIdentifier.fromInternal("t1")) + .withField(CqlIdentifier.fromInternal("i"), DataTypes.INT) + .build(); + private static final UserDefinedType OLD_T2 = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks1"), CqlIdentifier.fromInternal("t2")) + .withField(CqlIdentifier.fromInternal("i"), DataTypes.INT) + .build(); + private static final DefaultKeyspaceMetadata OLD_KS1 = newKeyspace("ks1", true, OLD_T1, OLD_T2); + + @Mock private InternalDriverContext context; + private DefaultMetadata oldMetadata; + + @Before + public void setup() { + oldMetadata = + DefaultMetadata.EMPTY.withSchema( + ImmutableMap.of(OLD_KS1.getName(), OLD_KS1), false, context); + } + + @Test + public void should_detect_dropped_keyspace() { + SchemaRefresh refresh = new SchemaRefresh(Collections.emptyMap()); + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + assertThat(result.newMetadata.getKeyspaces()).isEmpty(); + assertThat(result.events).containsExactly(KeyspaceChangeEvent.dropped(OLD_KS1)); + } + + @Test + public void should_detect_created_keyspace() { + DefaultKeyspaceMetadata ks2 = newKeyspace("ks2", true); + SchemaRefresh refresh = + new SchemaRefresh(ImmutableMap.of(OLD_KS1.getName(), OLD_KS1, ks2.getName(), ks2)); + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + assertThat(result.newMetadata.getKeyspaces()).hasSize(2); + assertThat(result.events).containsExactly(KeyspaceChangeEvent.created(ks2)); + } + + @Test + public void should_detect_top_level_update_in_keyspace() { + // Change only one top-level option (durable writes) + DefaultKeyspaceMetadata newKs1 = newKeyspace("ks1", false, OLD_T1, OLD_T2); + SchemaRefresh refresh = new SchemaRefresh(ImmutableMap.of(OLD_KS1.getName(), newKs1)); + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + assertThat(result.newMetadata.getKeyspaces()).hasSize(1); + assertThat(result.events).containsExactly(KeyspaceChangeEvent.updated(OLD_KS1, newKs1)); + } + + @Test + public void should_detect_updated_children_in_keyspace() { + // Drop one type, modify the other and add a third one + UserDefinedType newT2 = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks1"), CqlIdentifier.fromInternal("t2")) + .withField(CqlIdentifier.fromInternal("i"), DataTypes.TEXT) + .build(); + UserDefinedType t3 = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks1"), CqlIdentifier.fromInternal("t3")) + .withField(CqlIdentifier.fromInternal("i"), DataTypes.INT) + .build(); + DefaultKeyspaceMetadata newKs1 = newKeyspace("ks1", true, newT2, t3); + + SchemaRefresh refresh = new SchemaRefresh(ImmutableMap.of(OLD_KS1.getName(), newKs1)); + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + assertThat(result.newMetadata.getKeyspaces().get(OLD_KS1.getName())).isEqualTo(newKs1); + assertThat(result.events) + .containsExactly( + TypeChangeEvent.dropped(OLD_T1), + TypeChangeEvent.updated(OLD_T2, newT2), + TypeChangeEvent.created(t3)); + } + + @Test + public void should_detect_top_level_change_and_children_changes() { + // Drop one type, modify the other and add a third one + UserDefinedType newT2 = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks1"), CqlIdentifier.fromInternal("t2")) + .withField(CqlIdentifier.fromInternal("i"), DataTypes.TEXT) + .build(); + UserDefinedType t3 = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks1"), CqlIdentifier.fromInternal("t3")) + .withField(CqlIdentifier.fromInternal("i"), DataTypes.INT) + .build(); + // Also disable durable writes + DefaultKeyspaceMetadata newKs1 = newKeyspace("ks1", false, newT2, t3); + + SchemaRefresh refresh = new SchemaRefresh(ImmutableMap.of(OLD_KS1.getName(), newKs1)); + MetadataRefresh.Result result = refresh.compute(oldMetadata, false, context); + assertThat(result.newMetadata.getKeyspaces().get(OLD_KS1.getName())).isEqualTo(newKs1); + assertThat(result.events) + .containsExactly( + KeyspaceChangeEvent.updated(OLD_KS1, newKs1), + TypeChangeEvent.dropped(OLD_T1), + TypeChangeEvent.updated(OLD_T2, newT2), + TypeChangeEvent.created(t3)); + } + + private static DefaultKeyspaceMetadata newKeyspace( + String name, boolean durableWrites, UserDefinedType... userTypes) { + ImmutableMap.Builder typesMapBuilder = ImmutableMap.builder(); + for (UserDefinedType type : userTypes) { + typesMapBuilder.put(type.getName(), type); + } + return new DefaultKeyspaceMetadata( + CqlIdentifier.fromInternal(name), + durableWrites, + false, + Collections.emptyMap(), + typesMapBuilder.build(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap()); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedTokenRangeTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedTokenRangeTest.java new file mode 100644 index 00000000000..392a801c789 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/ByteOrderedTokenRangeTest.java @@ -0,0 +1,59 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.protocol.internal.util.Bytes; +import org.junit.Test; + +/** @see TokenRangeTest */ +public class ByteOrderedTokenRangeTest { + + private static final String MIN = "0x"; + + @Test + public void should_split_range() { + assertThat(range("0x0a", "0x0d").splitEvenly(3)) + .containsExactly(range("0x0a", "0x0b"), range("0x0b", "0x0c"), range("0x0c", "0x0d")); + } + + @Test + public void should_split_range_producing_empty_splits_near_ring_end() { + // 0x00 is the first token following min. + // This is an edge case where we want to make sure we don't accidentally generate the ]min,min] + // range (which is the whole ring): + assertThat(range(MIN, "0x00").splitEvenly(3)) + .containsExactly(range(MIN, "0x00"), range("0x00", "0x00"), range("0x00", "0x00")); + } + + @Test + public void should_split_range_that_wraps_around_the_ring() { + assertThat(range("0x0d", "0x0a").splitEvenly(2)) + .containsExactly(range("0x0d", "0x8c"), range("0x8c", "0x0a")); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_split_whole_ring() { + range(MIN, MIN).splitEvenly(1); + } + + private ByteOrderedTokenRange range(String start, String end) { + return new ByteOrderedTokenRange( + new ByteOrderedToken(Bytes.fromHexString(start)), + new ByteOrderedToken(Bytes.fromHexString(end))); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultTokenMapTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultTokenMapTest.java new file mode 100644 index 00000000000..fc4a8a3a7e5 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/DefaultTokenMapTest.java @@ -0,0 +1,382 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.schema.KeyspaceMetadata; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class DefaultTokenMapTest { + + private static final String DC1 = "DC1"; + private static final String DC2 = "DC2"; + private static final String RACK1 = "RACK1"; + private static final String RACK2 = "RACK2"; + + private static final CqlIdentifier KS1 = CqlIdentifier.fromInternal("ks1"); + private static final CqlIdentifier KS2 = CqlIdentifier.fromInternal("ks2"); + + private static final TokenFactory TOKEN_FACTORY = new Murmur3TokenFactory(); + + private static final String TOKEN1 = "-9000000000000000000"; + private static final String TOKEN2 = "-6000000000000000000"; + private static final String TOKEN3 = "4000000000000000000"; + private static final String TOKEN4 = "9000000000000000000"; + private static final TokenRange RANGE12 = range(TOKEN1, TOKEN2); + private static final TokenRange RANGE23 = range(TOKEN2, TOKEN3); + private static final TokenRange RANGE34 = range(TOKEN3, TOKEN4); + private static final TokenRange RANGE41 = range(TOKEN4, TOKEN1); + private static final TokenRange FULL_RING = + range(TOKEN_FACTORY.minToken(), TOKEN_FACTORY.minToken()); + + // Some random routing keys that land in the ranges above (they were generated manually) + private static ByteBuffer ROUTING_KEY12 = TypeCodecs.BIGINT.encode(2L, DefaultProtocolVersion.V3); + private static ByteBuffer ROUTING_KEY23 = TypeCodecs.BIGINT.encode(0L, DefaultProtocolVersion.V3); + private static ByteBuffer ROUTING_KEY34 = TypeCodecs.BIGINT.encode(1L, DefaultProtocolVersion.V3); + private static ByteBuffer ROUTING_KEY41 = + TypeCodecs.BIGINT.encode(99L, DefaultProtocolVersion.V3); + + private static final ImmutableMap REPLICATE_ON_BOTH_DCS = + ImmutableMap.of( + "class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DC1, "1", DC2, "1"); + private static final ImmutableMap REPLICATE_ON_DC1 = + ImmutableMap.of("class", "org.apache.cassandra.locator.NetworkTopologyStrategy", DC1, "1"); + + @Mock private InternalDriverContext context; + private ReplicationStrategyFactory replicationStrategyFactory; + + @Before + public void setup() { + replicationStrategyFactory = new DefaultReplicationStrategyFactory(context); + } + + @Test + public void should_build_token_map() { + // Given + Node node1 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN1)); + Node node2 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN2)); + Node node3 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN3)); + Node node4 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN4)); + List nodes = ImmutableList.of(node1, node2, node3, node4); + List keyspaces = + ImmutableList.of( + mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS), mockKeyspace(KS2, REPLICATE_ON_DC1)); + + // When + DefaultTokenMap tokenMap = + DefaultTokenMap.build(nodes, keyspaces, TOKEN_FACTORY, replicationStrategyFactory, "test"); + + // Then + assertThat(tokenMap.getTokenRanges()).containsExactly(RANGE12, RANGE23, RANGE34, RANGE41); + + // For KS1, each node gets its primary range, plus the one of the previous node in the other DC + assertThat(tokenMap.getTokenRanges(KS1, node1)).containsOnly(RANGE41, RANGE34); + assertThat(tokenMap.getTokenRanges(KS1, node2)).containsOnly(RANGE12, RANGE41); + assertThat(tokenMap.getTokenRanges(KS1, node3)).containsOnly(RANGE23, RANGE12); + assertThat(tokenMap.getTokenRanges(KS1, node4)).containsOnly(RANGE34, RANGE23); + + assertThat(tokenMap.getReplicas(KS1, RANGE12)).containsOnly(node2, node3); + assertThat(tokenMap.getReplicas(KS1, RANGE23)).containsOnly(node3, node4); + assertThat(tokenMap.getReplicas(KS1, RANGE34)).containsOnly(node1, node4); + assertThat(tokenMap.getReplicas(KS1, RANGE41)).containsOnly(node1, node2); + + assertThat(tokenMap.getReplicas(KS1, ROUTING_KEY12)).containsOnly(node2, node3); + assertThat(tokenMap.getReplicas(KS1, ROUTING_KEY23)).containsOnly(node3, node4); + assertThat(tokenMap.getReplicas(KS1, ROUTING_KEY34)).containsOnly(node1, node4); + assertThat(tokenMap.getReplicas(KS1, ROUTING_KEY41)).containsOnly(node1, node2); + + // KS2 is only replicated on DC1 + assertThat(tokenMap.getTokenRanges(KS2, node1)).containsOnly(RANGE41, RANGE34); + assertThat(tokenMap.getTokenRanges(KS2, node3)).containsOnly(RANGE23, RANGE12); + assertThat(tokenMap.getTokenRanges(KS2, node2)).isEmpty(); + assertThat(tokenMap.getTokenRanges(KS2, node4)).isEmpty(); + + assertThat(tokenMap.getReplicas(KS2, RANGE12)).containsOnly(node3); + assertThat(tokenMap.getReplicas(KS2, RANGE23)).containsOnly(node3); + assertThat(tokenMap.getReplicas(KS2, RANGE34)).containsOnly(node1); + assertThat(tokenMap.getReplicas(KS2, RANGE41)).containsOnly(node1); + + assertThat(tokenMap.getReplicas(KS2, ROUTING_KEY12)).containsOnly(node3); + assertThat(tokenMap.getReplicas(KS2, ROUTING_KEY23)).containsOnly(node3); + assertThat(tokenMap.getReplicas(KS2, ROUTING_KEY34)).containsOnly(node1); + assertThat(tokenMap.getReplicas(KS2, ROUTING_KEY41)).containsOnly(node1); + } + + @Test + public void should_build_token_map_with_single_node() { + // Given + Node node1 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN1)); + List nodes = ImmutableList.of(node1); + List keyspaces = + ImmutableList.of( + mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS), mockKeyspace(KS2, REPLICATE_ON_DC1)); + + // When + DefaultTokenMap tokenMap = + DefaultTokenMap.build(nodes, keyspaces, TOKEN_FACTORY, replicationStrategyFactory, "test"); + + // Then + assertThat(tokenMap.getTokenRanges()).containsExactly(FULL_RING); + + assertThat(tokenMap.getTokenRanges(KS1, node1)).containsOnly(FULL_RING); + assertThat(tokenMap.getReplicas(KS1, FULL_RING)).containsOnly(node1); + assertThat(tokenMap.getReplicas(KS1, ROUTING_KEY12)).containsOnly(node1); + assertThat(tokenMap.getReplicas(KS1, ROUTING_KEY23)).containsOnly(node1); + assertThat(tokenMap.getReplicas(KS1, ROUTING_KEY34)).containsOnly(node1); + assertThat(tokenMap.getReplicas(KS1, ROUTING_KEY41)).containsOnly(node1); + + assertThat(tokenMap.getTokenRanges(KS2, node1)).containsOnly(FULL_RING); + assertThat(tokenMap.getReplicas(KS2, FULL_RING)).containsOnly(node1); + assertThat(tokenMap.getReplicas(KS2, ROUTING_KEY12)).containsOnly(node1); + assertThat(tokenMap.getReplicas(KS2, ROUTING_KEY23)).containsOnly(node1); + assertThat(tokenMap.getReplicas(KS2, ROUTING_KEY34)).containsOnly(node1); + assertThat(tokenMap.getReplicas(KS2, ROUTING_KEY41)).containsOnly(node1); + } + + @Test + public void should_refresh_when_keyspace_replication_has_not_changed() { + // Given + Node node1 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN1)); + Node node2 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN2)); + Node node3 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN3)); + Node node4 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN4)); + List nodes = ImmutableList.of(node1, node2, node3, node4); + List oldKeyspaces = + ImmutableList.of( + mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS), mockKeyspace(KS2, REPLICATE_ON_DC1)); + DefaultTokenMap oldTokenMap = + DefaultTokenMap.build( + nodes, oldKeyspaces, TOKEN_FACTORY, replicationStrategyFactory, "test"); + + // When + // The schema gets refreshed, but no keyspaces are created or dropped, and the replication + // settings do not change (since we mock everything it looks the same here, but it could be a + // new table, etc). + List newKeyspaces = + ImmutableList.of( + mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS), mockKeyspace(KS2, REPLICATE_ON_DC1)); + DefaultTokenMap newTokenMap = + oldTokenMap.refresh(nodes, newKeyspaces, replicationStrategyFactory); + + // Then + // Nothing was recomputed + assertThat(newTokenMap.tokenRanges).isSameAs(oldTokenMap.tokenRanges); + assertThat(newTokenMap.tokenRangesByPrimary).isSameAs(oldTokenMap.tokenRangesByPrimary); + assertThat(newTokenMap.replicationConfigs).isSameAs(oldTokenMap.replicationConfigs); + assertThat(newTokenMap.keyspaceMaps).isSameAs(oldTokenMap.keyspaceMaps); + } + + @Test + public void should_refresh_when_new_keyspace_with_existing_replication() { + // Given + Node node1 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN1)); + Node node2 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN2)); + Node node3 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN3)); + Node node4 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN4)); + List nodes = ImmutableList.of(node1, node2, node3, node4); + List oldKeyspaces = + ImmutableList.of(mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS)); + DefaultTokenMap oldTokenMap = + DefaultTokenMap.build( + nodes, oldKeyspaces, TOKEN_FACTORY, replicationStrategyFactory, "test"); + assertThat(oldTokenMap.keyspaceMaps).containsOnlyKeys(REPLICATE_ON_BOTH_DCS); + + // When + List newKeyspaces = + ImmutableList.of( + mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS), mockKeyspace(KS2, REPLICATE_ON_BOTH_DCS)); + DefaultTokenMap newTokenMap = + oldTokenMap.refresh(nodes, newKeyspaces, replicationStrategyFactory); + + // Then + assertThat(newTokenMap.tokenRanges).isSameAs(oldTokenMap.tokenRanges); + assertThat(newTokenMap.tokenRangesByPrimary).isSameAs(oldTokenMap.tokenRangesByPrimary); + assertThat(newTokenMap.keyspaceMaps).isEqualTo(oldTokenMap.keyspaceMaps); + assertThat(newTokenMap.replicationConfigs) + .hasSize(2) + .containsEntry(KS1, REPLICATE_ON_BOTH_DCS) + .containsEntry(KS2, REPLICATE_ON_BOTH_DCS); + } + + @Test + public void should_refresh_when_new_keyspace_with_new_replication() { + // Given + Node node1 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN1)); + Node node2 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN2)); + Node node3 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN3)); + Node node4 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN4)); + List nodes = ImmutableList.of(node1, node2, node3, node4); + List oldKeyspaces = + ImmutableList.of(mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS)); + DefaultTokenMap oldTokenMap = + DefaultTokenMap.build( + nodes, oldKeyspaces, TOKEN_FACTORY, replicationStrategyFactory, "test"); + assertThat(oldTokenMap.keyspaceMaps).containsOnlyKeys(REPLICATE_ON_BOTH_DCS); + + // When + List newKeyspaces = + ImmutableList.of( + mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS), mockKeyspace(KS2, REPLICATE_ON_DC1)); + DefaultTokenMap newTokenMap = + oldTokenMap.refresh(nodes, newKeyspaces, replicationStrategyFactory); + + // Then + assertThat(newTokenMap.tokenRanges).isSameAs(oldTokenMap.tokenRanges); + assertThat(newTokenMap.tokenRangesByPrimary).isSameAs(oldTokenMap.tokenRangesByPrimary); + assertThat(newTokenMap.keyspaceMaps).containsOnlyKeys(REPLICATE_ON_BOTH_DCS, REPLICATE_ON_DC1); + assertThat(newTokenMap.replicationConfigs) + .hasSize(2) + .containsEntry(KS1, REPLICATE_ON_BOTH_DCS) + .containsEntry(KS2, REPLICATE_ON_DC1); + } + + @Test + public void should_refresh_when_dropped_keyspace_with_replication_still_used() { + // Given + Node node1 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN1)); + Node node2 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN2)); + Node node3 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN3)); + Node node4 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN4)); + List nodes = ImmutableList.of(node1, node2, node3, node4); + List oldKeyspaces = + ImmutableList.of( + mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS), mockKeyspace(KS2, REPLICATE_ON_BOTH_DCS)); + DefaultTokenMap oldTokenMap = + DefaultTokenMap.build( + nodes, oldKeyspaces, TOKEN_FACTORY, replicationStrategyFactory, "test"); + assertThat(oldTokenMap.keyspaceMaps).containsOnlyKeys(REPLICATE_ON_BOTH_DCS); + + // When + List newKeyspaces = + ImmutableList.of(mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS)); + DefaultTokenMap newTokenMap = + oldTokenMap.refresh(nodes, newKeyspaces, replicationStrategyFactory); + + // Then + assertThat(newTokenMap.tokenRanges).isSameAs(oldTokenMap.tokenRanges); + assertThat(newTokenMap.tokenRangesByPrimary).isSameAs(oldTokenMap.tokenRangesByPrimary); + assertThat(newTokenMap.keyspaceMaps).containsOnlyKeys(REPLICATE_ON_BOTH_DCS); + assertThat(newTokenMap.replicationConfigs).hasSize(1).containsEntry(KS1, REPLICATE_ON_BOTH_DCS); + } + + @Test + public void should_refresh_when_dropped_keyspace_with_replication_not_used_anymore() { + // Given + Node node1 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN1)); + Node node2 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN2)); + Node node3 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN3)); + Node node4 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN4)); + List nodes = ImmutableList.of(node1, node2, node3, node4); + List oldKeyspaces = + ImmutableList.of( + mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS), mockKeyspace(KS2, REPLICATE_ON_DC1)); + DefaultTokenMap oldTokenMap = + DefaultTokenMap.build( + nodes, oldKeyspaces, TOKEN_FACTORY, replicationStrategyFactory, "test"); + assertThat(oldTokenMap.keyspaceMaps).containsOnlyKeys(REPLICATE_ON_BOTH_DCS, REPLICATE_ON_DC1); + + // When + List newKeyspaces = + ImmutableList.of(mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS)); + DefaultTokenMap newTokenMap = + oldTokenMap.refresh(nodes, newKeyspaces, replicationStrategyFactory); + + // Then + assertThat(newTokenMap.tokenRanges).isSameAs(oldTokenMap.tokenRanges); + assertThat(newTokenMap.tokenRangesByPrimary).isSameAs(oldTokenMap.tokenRangesByPrimary); + assertThat(newTokenMap.keyspaceMaps).containsOnlyKeys(REPLICATE_ON_BOTH_DCS); + assertThat(newTokenMap.replicationConfigs).hasSize(1).containsEntry(KS1, REPLICATE_ON_BOTH_DCS); + } + + @Test + public void should_refresh_when_updated_keyspace_with_different_replication() { + // Given + Node node1 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN1)); + Node node2 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN2)); + Node node3 = mockNode(DC1, RACK1, ImmutableSet.of(TOKEN3)); + Node node4 = mockNode(DC2, RACK2, ImmutableSet.of(TOKEN4)); + List nodes = ImmutableList.of(node1, node2, node3, node4); + List oldKeyspaces = + ImmutableList.of( + mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS), mockKeyspace(KS2, REPLICATE_ON_DC1)); + DefaultTokenMap oldTokenMap = + DefaultTokenMap.build( + nodes, oldKeyspaces, TOKEN_FACTORY, replicationStrategyFactory, "test"); + assertThat(oldTokenMap.keyspaceMaps).containsOnlyKeys(REPLICATE_ON_BOTH_DCS, REPLICATE_ON_DC1); + + // When + List newKeyspaces = + ImmutableList.of( + mockKeyspace(KS1, REPLICATE_ON_BOTH_DCS), mockKeyspace(KS2, REPLICATE_ON_BOTH_DCS)); + DefaultTokenMap newTokenMap = + oldTokenMap.refresh(nodes, newKeyspaces, replicationStrategyFactory); + + // Then + assertThat(newTokenMap.tokenRanges).isSameAs(oldTokenMap.tokenRanges); + assertThat(newTokenMap.tokenRangesByPrimary).isSameAs(oldTokenMap.tokenRangesByPrimary); + assertThat(newTokenMap.keyspaceMaps).containsOnlyKeys(REPLICATE_ON_BOTH_DCS); + assertThat(newTokenMap.replicationConfigs) + .hasSize(2) + .containsEntry(KS1, REPLICATE_ON_BOTH_DCS) + .containsEntry(KS2, REPLICATE_ON_BOTH_DCS); + } + + private DefaultNode mockNode(String dc, String rack, Set tokens) { + DefaultNode node = mock(DefaultNode.class); + when(node.getDatacenter()).thenReturn(dc); + when(node.getRack()).thenReturn(rack); + when(node.getRawTokens()).thenReturn(tokens); + return node; + } + + private KeyspaceMetadata mockKeyspace(CqlIdentifier name, Map replicationConfig) { + KeyspaceMetadata keyspace = mock(KeyspaceMetadata.class); + when(keyspace.getName()).thenReturn(name); + when(keyspace.getReplication()).thenReturn(replicationConfig); + return keyspace; + } + + private static TokenRange range(String start, String end) { + return range(TOKEN_FACTORY.parse(start), TOKEN_FACTORY.parse(end)); + } + + private static TokenRange range(Token startToken, Token endToken) { + return new Murmur3TokenRange((Murmur3Token) startToken, (Murmur3Token) endToken); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3TokenRangeTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3TokenRangeTest.java new file mode 100644 index 00000000000..3d14f21c741 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/Murmur3TokenRangeTest.java @@ -0,0 +1,80 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import org.junit.Test; + +/** @see TokenRangeTest */ +public class Murmur3TokenRangeTest { + + private static final long MIN = -9223372036854775808L; + private static final long MAX = 9223372036854775807L; + + @Test + public void should_split_range() { + assertThat(range(MIN, 4611686018427387904L).splitEvenly(3)) + .containsExactly( + range(MIN, -4611686018427387904L), + range(-4611686018427387904L, 0), + range(0, 4611686018427387904L)); + } + + @Test + public void should_split_range_that_wraps_around_the_ring() { + assertThat(range(4611686018427387904L, 0).splitEvenly(3)) + .containsExactly( + range(4611686018427387904L, -9223372036854775807L), + range(-9223372036854775807L, -4611686018427387903L), + range(-4611686018427387903L, 0)); + } + + @Test + public void should_split_range_when_division_not_integral() { + assertThat(range(0, 11).splitEvenly(3)).containsExactly(range(0, 4), range(4, 8), range(8, 11)); + } + + @Test + public void should_split_range_producing_empty_splits() { + assertThat(range(0, 2).splitEvenly(5)) + .containsExactly(range(0, 1), range(1, 2), range(2, 2), range(2, 2), range(2, 2)); + } + + @Test + public void should_split_range_producing_empty_splits_near_ring_end() { + // These are edge cases where we want to make sure we don't accidentally generate the ]min,min] + // range (which is the whole ring) + assertThat(range(MAX, MIN).splitEvenly(3)) + .containsExactly(range(MAX, MAX), range(MAX, MAX), range(MAX, MIN)); + + assertThat(range(MIN, MIN + 1).splitEvenly(3)) + .containsExactly(range(MIN, MIN + 1), range(MIN + 1, MIN + 1), range(MIN + 1, MIN + 1)); + } + + @Test + public void should_split_whole_ring() { + assertThat(range(MIN, MIN).splitEvenly(3)) + .containsExactly( + range(MIN, -3074457345618258603L), + range(-3074457345618258603L, 3074457345618258602L), + range(3074457345618258602L, MIN)); + } + + private Murmur3TokenRange range(long start, long end) { + return new Murmur3TokenRange(new Murmur3Token(start), new Murmur3Token(end)); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/NetworkTopologyReplicationStrategyTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/NetworkTopologyReplicationStrategyTest.java new file mode 100644 index 00000000000..01627628609 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/NetworkTopologyReplicationStrategyTest.java @@ -0,0 +1,703 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.SetMultimap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.slf4j.LoggerFactory; + +@RunWith(MockitoJUnitRunner.class) +public class NetworkTopologyReplicationStrategyTest { + + private static final String DC1 = "DC1"; + private static final String DC2 = "DC2"; + private static final String DC3 = "DC3"; + private static final String RACK11 = "RACK11"; + private static final String RACK12 = "RACK12"; + private static final String RACK21 = "RACK21"; + private static final String RACK22 = "RACK22"; + private static final String RACK31 = "RACK31"; + + private static final Token TOKEN01 = new Murmur3Token(-9000000000000000000L); + private static final Token TOKEN02 = new Murmur3Token(-8000000000000000000L); + private static final Token TOKEN03 = new Murmur3Token(-7000000000000000000L); + private static final Token TOKEN04 = new Murmur3Token(-6000000000000000000L); + private static final Token TOKEN05 = new Murmur3Token(-5000000000000000000L); + private static final Token TOKEN06 = new Murmur3Token(-4000000000000000000L); + private static final Token TOKEN07 = new Murmur3Token(-3000000000000000000L); + private static final Token TOKEN08 = new Murmur3Token(-2000000000000000000L); + private static final Token TOKEN09 = new Murmur3Token(-1000000000000000000L); + private static final Token TOKEN10 = new Murmur3Token(0L); + private static final Token TOKEN11 = new Murmur3Token(1000000000000000000L); + private static final Token TOKEN12 = new Murmur3Token(2000000000000000000L); + private static final Token TOKEN13 = new Murmur3Token(3000000000000000000L); + private static final Token TOKEN14 = new Murmur3Token(4000000000000000000L); + private static final Token TOKEN15 = new Murmur3Token(5000000000000000000L); + private static final Token TOKEN16 = new Murmur3Token(6000000000000000000L); + private static final Token TOKEN17 = new Murmur3Token(7000000000000000000L); + private static final Token TOKEN18 = new Murmur3Token(8000000000000000000L); + private static final Token TOKEN19 = new Murmur3Token(9000000000000000000L); + + @Mock private Node node1, node2, node3, node4, node5, node6, node7, node8; + + @Mock private Appender appender; + @Captor private ArgumentCaptor loggingEventCaptor; + + /** 4 tokens, 2 nodes in 2 DCs, RF = 1 in each DC. */ + @Test + public void should_compute_for_simple_layout() { + // Given + List ring = ImmutableList.of(TOKEN01, TOKEN04, TOKEN14, TOKEN19); + locate(node1, DC1, RACK11); + locate(node2, DC2, RACK21); + Map tokenToPrimary = + ImmutableMap.of(TOKEN01, node1, TOKEN04, node2, TOKEN14, node1, TOKEN19, node2); + ReplicationStrategy strategy = + new NetworkTopologyReplicationStrategy(ImmutableMap.of(DC1, "1", DC2, "1"), "test"); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + // Note: this also asserts the iteration order of the sets (unlike containsEntry(token, set)) + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN04)).containsExactly(node2, node1); + assertThat(replicasByToken.get(TOKEN14)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN19)).containsExactly(node2, node1); + } + + /** 8 tokens, 4 nodes in 2 DCs in the same racks, RF = 1 in each DC. */ + @Test + public void should_compute_for_simple_layout_with_multiple_nodes_per_rack() { + // Given + List ring = + ImmutableList.of(TOKEN01, TOKEN03, TOKEN05, TOKEN07, TOKEN13, TOKEN15, TOKEN17, TOKEN19); + locate(node1, DC1, RACK11); + locate(node2, DC2, RACK21); + locate(node3, DC1, RACK11); + locate(node4, DC2, RACK21); + Map tokenToPrimary = + ImmutableMap.builder() + .put(TOKEN01, node1) + .put(TOKEN03, node2) + .put(TOKEN05, node3) + .put(TOKEN07, node4) + .put(TOKEN13, node1) + .put(TOKEN15, node2) + .put(TOKEN17, node3) + .put(TOKEN19, node4) + .build(); + ReplicationStrategy strategy = + new NetworkTopologyReplicationStrategy(ImmutableMap.of(DC1, "1", DC2, "1"), "test"); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN03)).containsExactly(node2, node3); + assertThat(replicasByToken.get(TOKEN05)).containsExactly(node3, node4); + assertThat(replicasByToken.get(TOKEN07)).containsExactly(node4, node1); + assertThat(replicasByToken.get(TOKEN13)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN15)).containsExactly(node2, node3); + assertThat(replicasByToken.get(TOKEN17)).containsExactly(node3, node4); + assertThat(replicasByToken.get(TOKEN19)).containsExactly(node4, node1); + } + + /** 6 tokens, 3 nodes in 3 DCs, RF = 1 in each DC. */ + @Test + public void should_compute_for_simple_layout_with_3_dcs() { + // Given + List ring = ImmutableList.of(TOKEN01, TOKEN05, TOKEN09, TOKEN11, TOKEN15, TOKEN19); + locate(node1, DC1, RACK11); + locate(node2, DC2, RACK21); + locate(node3, DC3, RACK31); + Map tokenToPrimary = + ImmutableMap.builder() + .put(TOKEN01, node1) + .put(TOKEN05, node2) + .put(TOKEN09, node3) + .put(TOKEN11, node1) + .put(TOKEN15, node2) + .put(TOKEN19, node3) + .build(); + ReplicationStrategy strategy = + new NetworkTopologyReplicationStrategy( + ImmutableMap.of(DC1, "1", DC2, "1", DC3, "1"), "test"); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node2, node3); + assertThat(replicasByToken.get(TOKEN05)).containsExactly(node2, node3, node1); + assertThat(replicasByToken.get(TOKEN09)).containsExactly(node3, node1, node2); + assertThat(replicasByToken.get(TOKEN11)).containsExactly(node1, node2, node3); + assertThat(replicasByToken.get(TOKEN15)).containsExactly(node2, node3, node1); + assertThat(replicasByToken.get(TOKEN19)).containsExactly(node3, node1, node2); + } + + /** 10 tokens, 4 nodes in 2 DCs, RF = 2 in each DC, 1 node owns 4 tokens, the others only 2. */ + @Test + public void should_compute_for_unbalanced_ring() { + // Given + List ring = + ImmutableList.of( + TOKEN01, TOKEN03, TOKEN05, TOKEN07, TOKEN09, TOKEN11, TOKEN13, TOKEN15, TOKEN17, + TOKEN19); + locate(node1, DC1, RACK11); + locate(node2, DC2, RACK21); + locate(node3, DC1, RACK11); + locate(node4, DC2, RACK21); + Map tokenToPrimary = + ImmutableMap.builder() + .put(TOKEN01, node1) + .put(TOKEN03, node1) + .put(TOKEN05, node2) + .put(TOKEN07, node3) + .put(TOKEN09, node4) + .put(TOKEN11, node1) + .put(TOKEN13, node1) + .put(TOKEN15, node2) + .put(TOKEN17, node3) + .put(TOKEN19, node4) + .build(); + ReplicationStrategy strategy = + new NetworkTopologyReplicationStrategy(ImmutableMap.of(DC1, "2", DC2, "2"), "test"); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node2, node3, node4); + assertThat(replicasByToken.get(TOKEN03)).containsExactly(node1, node2, node3, node4); + assertThat(replicasByToken.get(TOKEN05)).containsExactly(node2, node3, node4, node1); + assertThat(replicasByToken.get(TOKEN07)).containsExactly(node3, node4, node1, node2); + assertThat(replicasByToken.get(TOKEN09)).containsExactly(node4, node1, node2, node3); + assertThat(replicasByToken.get(TOKEN11)).containsExactly(node1, node2, node3, node4); + assertThat(replicasByToken.get(TOKEN13)).containsExactly(node1, node2, node3, node4); + assertThat(replicasByToken.get(TOKEN15)).containsExactly(node2, node3, node4, node1); + assertThat(replicasByToken.get(TOKEN17)).containsExactly(node3, node4, node1, node2); + assertThat(replicasByToken.get(TOKEN19)).containsExactly(node4, node1, node2, node3); + } + + /** 16 tokens, 8 nodes in 2 DCs with 2 per rack, RF = 2 in each DC. */ + @Test + public void should_compute_with_multiple_racks_per_dc() { + // Given + List ring = + ImmutableList.of( + TOKEN01, TOKEN02, TOKEN03, TOKEN04, TOKEN05, TOKEN06, TOKEN07, TOKEN08, TOKEN12, + TOKEN13, TOKEN14, TOKEN15, TOKEN16, TOKEN17, TOKEN18, TOKEN19); + locate(node1, DC1, RACK11); + locate(node2, DC2, RACK21); + locate(node3, DC1, RACK12); + locate(node4, DC2, RACK22); + locate(node5, DC1, RACK11); + locate(node6, DC2, RACK21); + locate(node7, DC1, RACK12); + locate(node8, DC2, RACK22); + Map tokenToPrimary = + ImmutableMap.builder() + .put(TOKEN01, node1) + .put(TOKEN02, node2) + .put(TOKEN03, node3) + .put(TOKEN04, node4) + .put(TOKEN05, node5) + .put(TOKEN06, node6) + .put(TOKEN07, node7) + .put(TOKEN08, node8) + .put(TOKEN12, node1) + .put(TOKEN13, node2) + .put(TOKEN14, node3) + .put(TOKEN15, node4) + .put(TOKEN16, node5) + .put(TOKEN17, node6) + .put(TOKEN18, node7) + .put(TOKEN19, node8) + .build(); + ReplicationStrategy strategy = + new NetworkTopologyReplicationStrategy(ImmutableMap.of(DC1, "2", DC2, "2"), "test"); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node2, node3, node4); + assertThat(replicasByToken.get(TOKEN02)).containsExactly(node2, node3, node4, node5); + assertThat(replicasByToken.get(TOKEN03)).containsExactly(node3, node4, node5, node6); + assertThat(replicasByToken.get(TOKEN04)).containsExactly(node4, node5, node6, node7); + assertThat(replicasByToken.get(TOKEN05)).containsExactly(node5, node6, node7, node8); + assertThat(replicasByToken.get(TOKEN06)).containsExactly(node6, node7, node8, node1); + assertThat(replicasByToken.get(TOKEN07)).containsExactly(node7, node8, node1, node2); + assertThat(replicasByToken.get(TOKEN08)).containsExactly(node8, node1, node2, node3); + assertThat(replicasByToken.get(TOKEN12)).containsExactly(node1, node2, node3, node4); + assertThat(replicasByToken.get(TOKEN13)).containsExactly(node2, node3, node4, node5); + assertThat(replicasByToken.get(TOKEN14)).containsExactly(node3, node4, node5, node6); + assertThat(replicasByToken.get(TOKEN15)).containsExactly(node4, node5, node6, node7); + assertThat(replicasByToken.get(TOKEN16)).containsExactly(node5, node6, node7, node8); + assertThat(replicasByToken.get(TOKEN17)).containsExactly(node6, node7, node8, node1); + assertThat(replicasByToken.get(TOKEN18)).containsExactly(node7, node8, node1, node2); + assertThat(replicasByToken.get(TOKEN19)).containsExactly(node8, node1, node2, node3); + } + + /** + * 16 tokens, 8 nodes in 2 DCs with 2 per rack, RF = 3 in each DC. + * + *

The nodes that are in the same rack occupy consecutive positions on the ring. We want to + * reproduce the case where we hit the same rack when we look for the second replica of a DC; the + * expected behavior is to skip the node and go to the next rack, and come back to the first rack + * for the third replica. + */ + @Test + public void should_pick_dc_replicas_in_different_racks_first() { + // Given + List ring = + ImmutableList.of( + TOKEN01, TOKEN02, TOKEN03, TOKEN04, TOKEN05, TOKEN06, TOKEN07, TOKEN08, TOKEN12, + TOKEN13, TOKEN14, TOKEN15, TOKEN16, TOKEN17, TOKEN18, TOKEN19); + locate(node1, DC1, RACK11); + locate(node2, DC2, RACK21); + locate(node3, DC1, RACK11); + locate(node4, DC2, RACK21); + locate(node5, DC1, RACK12); + locate(node6, DC2, RACK22); + locate(node7, DC1, RACK12); + locate(node8, DC2, RACK22); + Map tokenToPrimary = + ImmutableMap.builder() + .put(TOKEN01, node1) + .put(TOKEN02, node2) + .put(TOKEN03, node3) + .put(TOKEN04, node4) + .put(TOKEN05, node5) + .put(TOKEN06, node6) + .put(TOKEN07, node7) + .put(TOKEN08, node8) + .put(TOKEN12, node1) + .put(TOKEN13, node2) + .put(TOKEN14, node3) + .put(TOKEN15, node4) + .put(TOKEN16, node5) + .put(TOKEN17, node6) + .put(TOKEN18, node7) + .put(TOKEN19, node8) + .build(); + ReplicationStrategy strategy = + new NetworkTopologyReplicationStrategy(ImmutableMap.of(DC1, "3", DC2, "3"), "test"); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + assertThat(replicasByToken.get(TOKEN01)) + .containsExactly(node1, node2, node5, node3, node6, node4); + assertThat(replicasByToken.get(TOKEN02)) + .containsExactly(node2, node3, node5, node6, node4, node7); + assertThat(replicasByToken.get(TOKEN03)) + .containsExactly(node3, node4, node5, node6, node7, node8); + assertThat(replicasByToken.get(TOKEN04)) + .containsExactly(node4, node5, node6, node8, node1, node7); + assertThat(replicasByToken.get(TOKEN05)) + .containsExactly(node5, node6, node1, node7, node2, node8); + assertThat(replicasByToken.get(TOKEN06)) + .containsExactly(node6, node7, node1, node2, node8, node3); + assertThat(replicasByToken.get(TOKEN07)) + .containsExactly(node7, node8, node1, node2, node3, node4); + assertThat(replicasByToken.get(TOKEN08)) + .containsExactly(node8, node1, node2, node4, node5, node3); + assertThat(replicasByToken.get(TOKEN12)) + .containsExactly(node1, node2, node5, node3, node6, node4); + assertThat(replicasByToken.get(TOKEN13)) + .containsExactly(node2, node3, node5, node6, node4, node7); + assertThat(replicasByToken.get(TOKEN14)) + .containsExactly(node3, node4, node5, node6, node7, node8); + assertThat(replicasByToken.get(TOKEN15)) + .containsExactly(node4, node5, node6, node8, node1, node7); + assertThat(replicasByToken.get(TOKEN16)) + .containsExactly(node5, node6, node1, node7, node2, node8); + assertThat(replicasByToken.get(TOKEN17)) + .containsExactly(node6, node7, node1, node2, node8, node3); + assertThat(replicasByToken.get(TOKEN18)) + .containsExactly(node7, node8, node1, node2, node3, node4); + assertThat(replicasByToken.get(TOKEN19)) + .containsExactly(node8, node1, node2, node4, node5, node3); + } + + /** + * 16 tokens, 8 nodes in 2 DCs with 2 per rack, RF = 3 in each DC. + * + *

This is the same scenario as {@link #should_pick_dc_replicas_in_different_racks_first()}, + * except that each node owns consecutive tokens on the ring. + */ + @Test + public void should_pick_dc_replicas_in_different_racks_first_when_nodes_own_consecutive_tokens() { + // When + SetMultimap replicasByToken = computeWithDifferentRacksAndConsecutiveTokens(3); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(16); + assertThat(replicasByToken.get(TOKEN01)) + .containsExactly(node1, node5, node3, node2, node6, node4); + assertThat(replicasByToken.get(TOKEN02)) + .containsExactly(node1, node5, node3, node2, node6, node4); + assertThat(replicasByToken.get(TOKEN03)) + .containsExactly(node3, node5, node7, node2, node6, node4); + assertThat(replicasByToken.get(TOKEN04)) + .containsExactly(node3, node5, node7, node2, node6, node4); + assertThat(replicasByToken.get(TOKEN05)) + .containsExactly(node5, node2, node6, node4, node1, node7); + assertThat(replicasByToken.get(TOKEN06)) + .containsExactly(node5, node2, node6, node4, node1, node7); + assertThat(replicasByToken.get(TOKEN07)) + .containsExactly(node7, node2, node6, node4, node1, node3); + assertThat(replicasByToken.get(TOKEN08)) + .containsExactly(node7, node2, node6, node4, node1, node3); + assertThat(replicasByToken.get(TOKEN12)) + .containsExactly(node2, node6, node4, node1, node5, node3); + assertThat(replicasByToken.get(TOKEN13)) + .containsExactly(node2, node6, node4, node1, node5, node3); + assertThat(replicasByToken.get(TOKEN14)) + .containsExactly(node4, node6, node8, node1, node5, node3); + assertThat(replicasByToken.get(TOKEN15)) + .containsExactly(node4, node6, node8, node1, node5, node3); + assertThat(replicasByToken.get(TOKEN16)) + .containsExactly(node6, node1, node5, node3, node2, node8); + assertThat(replicasByToken.get(TOKEN17)) + .containsExactly(node6, node1, node5, node3, node2, node8); + assertThat(replicasByToken.get(TOKEN18)) + .containsExactly(node8, node1, node5, node3, node2, node4); + assertThat(replicasByToken.get(TOKEN19)) + .containsExactly(node8, node1, node5, node3, node2, node4); + } + + /** + * 16 tokens, 8 nodes in 2 DCs with 2 per rack, RF = 4 in each DC. + * + *

This is the same test as {@link + * #should_pick_dc_replicas_in_different_racks_first_when_nodes_own_consecutive_tokens()}, except + * for the replication factors. + */ + @Test + public void should_pick_dc_replicas_in_different_racks_first_when_all_nodes_contain_all_data() { + // When + SetMultimap replicasByToken = computeWithDifferentRacksAndConsecutiveTokens(4); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(16); + assertThat(replicasByToken.get(TOKEN01)) + .containsExactly(node1, node5, node3, node7, node2, node6, node4, node8); + assertThat(replicasByToken.get(TOKEN02)) + .containsExactly(node1, node5, node3, node7, node2, node6, node4, node8); + assertThat(replicasByToken.get(TOKEN03)) + .containsExactly(node3, node5, node7, node2, node6, node4, node8, node1); + assertThat(replicasByToken.get(TOKEN04)) + .containsExactly(node3, node5, node7, node2, node6, node4, node8, node1); + assertThat(replicasByToken.get(TOKEN05)) + .containsExactly(node5, node2, node6, node4, node8, node1, node7, node3); + assertThat(replicasByToken.get(TOKEN06)) + .containsExactly(node5, node2, node6, node4, node8, node1, node7, node3); + assertThat(replicasByToken.get(TOKEN07)) + .containsExactly(node7, node2, node6, node4, node8, node1, node3, node5); + assertThat(replicasByToken.get(TOKEN08)) + .containsExactly(node7, node2, node6, node4, node8, node1, node3, node5); + assertThat(replicasByToken.get(TOKEN12)) + .containsExactly(node2, node6, node4, node8, node1, node5, node3, node7); + assertThat(replicasByToken.get(TOKEN13)) + .containsExactly(node2, node6, node4, node8, node1, node5, node3, node7); + assertThat(replicasByToken.get(TOKEN14)) + .containsExactly(node4, node6, node8, node1, node5, node3, node7, node2); + assertThat(replicasByToken.get(TOKEN15)) + .containsExactly(node4, node6, node8, node1, node5, node3, node7, node2); + assertThat(replicasByToken.get(TOKEN16)) + .containsExactly(node6, node1, node5, node3, node7, node2, node8, node4); + assertThat(replicasByToken.get(TOKEN17)) + .containsExactly(node6, node1, node5, node3, node7, node2, node8, node4); + assertThat(replicasByToken.get(TOKEN18)) + .containsExactly(node8, node1, node5, node3, node7, node2, node4, node6); + assertThat(replicasByToken.get(TOKEN19)) + .containsExactly(node8, node1, node5, node3, node7, node2, node4, node6); + } + + private SetMultimap computeWithDifferentRacksAndConsecutiveTokens( + int replicationFactor) { + List ring = + ImmutableList.of( + TOKEN01, TOKEN02, TOKEN03, TOKEN04, TOKEN05, TOKEN06, TOKEN07, TOKEN08, TOKEN12, + TOKEN13, TOKEN14, TOKEN15, TOKEN16, TOKEN17, TOKEN18, TOKEN19); + locate(node1, DC1, RACK11); + locate(node2, DC2, RACK21); + locate(node3, DC1, RACK11); + locate(node4, DC2, RACK21); + locate(node5, DC1, RACK12); + locate(node6, DC2, RACK22); + locate(node7, DC1, RACK12); + locate(node8, DC2, RACK22); + Map tokenToPrimary = + ImmutableMap.builder() + .put(TOKEN01, node1) + .put(TOKEN02, node1) + .put(TOKEN03, node3) + .put(TOKEN04, node3) + .put(TOKEN05, node5) + .put(TOKEN06, node5) + .put(TOKEN07, node7) + .put(TOKEN08, node7) + .put(TOKEN12, node2) + .put(TOKEN13, node2) + .put(TOKEN14, node4) + .put(TOKEN15, node4) + .put(TOKEN16, node6) + .put(TOKEN17, node6) + .put(TOKEN18, node8) + .put(TOKEN19, node8) + .build(); + ReplicationStrategy strategy = + new NetworkTopologyReplicationStrategy( + ImmutableMap.of( + DC1, Integer.toString(replicationFactor), DC2, Integer.toString(replicationFactor)), + "test"); + + return strategy.computeReplicasByToken(tokenToPrimary, ring); + } + + /** + * 18 tokens, 6 nodes in 2 DCs with 2 in rack 1 and 1 in rack 2, RF = 2 in each DC. + * + *

This is taken from a real-life cluster. + */ + @Test + public void should_compute_complex_layout() { + // When + SetMultimap replicasByToken = computeComplexLayout(2); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(18); + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node5, node2, node6); + assertThat(replicasByToken.get(TOKEN02)).containsExactly(node1, node5, node2, node6); + assertThat(replicasByToken.get(TOKEN03)).containsExactly(node5, node3, node2, node6); + assertThat(replicasByToken.get(TOKEN04)).containsExactly(node3, node5, node2, node6); + assertThat(replicasByToken.get(TOKEN05)).containsExactly(node1, node5, node2, node6); + assertThat(replicasByToken.get(TOKEN06)).containsExactly(node5, node2, node6, node3); + assertThat(replicasByToken.get(TOKEN07)).containsExactly(node2, node6, node3, node5); + assertThat(replicasByToken.get(TOKEN08)).containsExactly(node6, node3, node4, node5); + assertThat(replicasByToken.get(TOKEN09)).containsExactly(node3, node4, node5, node6); + assertThat(replicasByToken.get(TOKEN10)).containsExactly(node4, node5, node6, node3); + assertThat(replicasByToken.get(TOKEN11)).containsExactly(node5, node4, node6, node3); + assertThat(replicasByToken.get(TOKEN12)).containsExactly(node4, node6, node3, node5); + assertThat(replicasByToken.get(TOKEN13)).containsExactly(node4, node6, node3, node5); + assertThat(replicasByToken.get(TOKEN14)).containsExactly(node2, node6, node3, node5); + assertThat(replicasByToken.get(TOKEN15)).containsExactly(node6, node3, node2, node5); + assertThat(replicasByToken.get(TOKEN16)).containsExactly(node3, node2, node6, node5); + assertThat(replicasByToken.get(TOKEN17)).containsExactly(node2, node6, node1, node5); + assertThat(replicasByToken.get(TOKEN18)).containsExactly(node6, node1, node5, node2); + } + + /** + * 18 tokens, 6 nodes in 2 DCs with 2 in rack 1 and 1 in rack 2, RF = 4 in each DC. + * + *

This is the same test as {@link #should_compute_complex_layout()}, but with RF = 4, which is + * too high for this cluster (it would require 8 nodes). + */ + @Test + public void should_compute_complex_layout_with_rf_too_high() { + // When + SetMultimap replicasByToken = computeComplexLayout(4); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(18); + assertThat(replicasByToken.get(TOKEN01)) + .containsExactly(node1, node5, node3, node2, node6, node4); + assertThat(replicasByToken.get(TOKEN02)) + .containsExactly(node1, node5, node3, node2, node6, node4); + assertThat(replicasByToken.get(TOKEN03)) + .containsExactly(node5, node3, node1, node2, node6, node4); + assertThat(replicasByToken.get(TOKEN04)) + .containsExactly(node3, node5, node1, node2, node6, node4); + assertThat(replicasByToken.get(TOKEN05)) + .containsExactly(node1, node5, node2, node6, node3, node4); + assertThat(replicasByToken.get(TOKEN06)) + .containsExactly(node5, node2, node6, node3, node4, node1); + assertThat(replicasByToken.get(TOKEN07)) + .containsExactly(node2, node6, node3, node4, node5, node1); + assertThat(replicasByToken.get(TOKEN08)) + .containsExactly(node6, node3, node4, node5, node2, node1); + assertThat(replicasByToken.get(TOKEN09)) + .containsExactly(node3, node4, node5, node6, node2, node1); + assertThat(replicasByToken.get(TOKEN10)) + .containsExactly(node4, node5, node6, node2, node3, node1); + assertThat(replicasByToken.get(TOKEN11)) + .containsExactly(node5, node4, node6, node2, node3, node1); + assertThat(replicasByToken.get(TOKEN12)) + .containsExactly(node4, node6, node2, node3, node5, node1); + assertThat(replicasByToken.get(TOKEN13)) + .containsExactly(node4, node6, node2, node3, node5, node1); + assertThat(replicasByToken.get(TOKEN14)) + .containsExactly(node2, node6, node3, node5, node1, node4); + assertThat(replicasByToken.get(TOKEN15)) + .containsExactly(node6, node3, node2, node5, node1, node4); + assertThat(replicasByToken.get(TOKEN16)) + .containsExactly(node3, node2, node6, node5, node1, node4); + assertThat(replicasByToken.get(TOKEN17)) + .containsExactly(node2, node6, node1, node5, node3, node4); + assertThat(replicasByToken.get(TOKEN18)) + .containsExactly(node6, node1, node5, node3, node2, node4); + } + + private SetMultimap computeComplexLayout(int replicationFactor) { + List ring = + ImmutableList.of( + TOKEN01, TOKEN02, TOKEN03, TOKEN04, TOKEN05, TOKEN06, TOKEN07, TOKEN08, TOKEN09, + TOKEN10, TOKEN11, TOKEN12, TOKEN13, TOKEN14, TOKEN15, TOKEN16, TOKEN17, TOKEN18); + locate(node1, DC1, RACK11); + locate(node2, DC2, RACK21); + locate(node3, DC1, RACK11); + locate(node4, DC2, RACK21); + locate(node5, DC1, RACK12); + locate(node6, DC2, RACK22); + Map tokenToPrimary = + ImmutableMap.builder() + .put(TOKEN01, node1) + .put(TOKEN02, node1) + .put(TOKEN03, node5) + .put(TOKEN04, node3) + .put(TOKEN05, node1) + .put(TOKEN06, node5) + .put(TOKEN07, node2) + .put(TOKEN08, node6) + .put(TOKEN09, node3) + .put(TOKEN10, node4) + .put(TOKEN11, node5) + .put(TOKEN12, node4) + .put(TOKEN13, node4) + .put(TOKEN14, node2) + .put(TOKEN15, node6) + .put(TOKEN16, node3) + .put(TOKEN17, node2) + .put(TOKEN18, node6) + .build(); + ReplicationStrategy strategy = + new NetworkTopologyReplicationStrategy( + ImmutableMap.of( + DC1, Integer.toString(replicationFactor), DC2, Integer.toString(replicationFactor)), + "test"); + + return strategy.computeReplicasByToken(tokenToPrimary, ring); + } + + /** + * When the replication factors are invalid (user error) and a datacenter has a replication factor + * that cannot be met, we want to quickly abort and move on to the next DC (instead of keeping + * scanning the ring in vain, which results in quadratic complexity). We also log a warning to + * give the user a chance to fix their settings. + * + * @see JAVA-702 + * @see JAVA-859 + */ + @Test + public void should_abort_early_and_log_when_bad_replication_factor_cannot_be_met() { + // Given + List ring = ImmutableList.of(TOKEN01, TOKEN04, TOKEN14, TOKEN19); + locate(node1, DC1, RACK11); + locate(node2, DC2, RACK21); + Map tokenToPrimary = + ImmutableMap.of(TOKEN01, node1, TOKEN04, node2, TOKEN14, node1, TOKEN19, node2); + Logger logger = (Logger) LoggerFactory.getLogger(NetworkTopologyReplicationStrategy.class); + logger.addAppender(appender); + + try { + // When + int traversedTokensForValidSettings = + countTraversedTokens(ring, tokenToPrimary, ImmutableMap.of(DC1, "1", DC2, "1")); + + // Then + // No logs: + verify(appender, never()).doAppend(any(ILoggingEvent.class)); + + // When + int traversedTokensForInvalidSettings = + countTraversedTokens(ring, tokenToPrimary, ImmutableMap.of(DC1, "1", DC2, "1", DC3, "1")); + // Did not take more steps than the valid settings + assertThat(traversedTokensForInvalidSettings).isEqualTo(traversedTokensForValidSettings); + // Did log: + verify(appender).doAppend(loggingEventCaptor.capture()); + ILoggingEvent log = loggingEventCaptor.getValue(); + assertThat(log.getLevel()).isEqualTo(Level.WARN); + assertThat(log.getMessage()).contains("could not achieve replication factor"); + } finally { + logger.detachAppender(appender); + } + } + + // Counts the number of steps on the ring for a particular computation + private int countTraversedTokens( + List ring, + Map tokenToPrimary, + ImmutableMap replicationConfig) { + AtomicInteger count = new AtomicInteger(); + List ringSpy = spy(ring); + when(ringSpy.get(anyInt())) + .thenAnswer( + invocation -> { + count.incrementAndGet(); + return invocation.callRealMethod(); + }); + new NetworkTopologyReplicationStrategy(replicationConfig, "test") + .computeReplicasByToken(tokenToPrimary, ringSpy); + return count.get(); + } + + private void locate(Node node, String dc, String rack) { + when(node.getDatacenter()).thenReturn(dc); + when(node.getRack()).thenReturn(rack); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/RandomTokenRangeTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/RandomTokenRangeTest.java new file mode 100644 index 00000000000..57a22f463cf --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/RandomTokenRangeTest.java @@ -0,0 +1,81 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigInteger; +import org.junit.Test; + +public class RandomTokenRangeTest { + + private static final String MIN = "-1"; + private static final String MAX = "170141183460469231731687303715884105728"; + + @Test + public void should_split_range() { + assertThat(range("0", "127605887595351923798765477786913079296").splitEvenly(3)) + .containsExactly( + range("0", "42535295865117307932921825928971026432"), + range( + "42535295865117307932921825928971026432", "85070591730234615865843651857942052864"), + range( + "85070591730234615865843651857942052864", + "127605887595351923798765477786913079296")); + } + + @Test + public void should_split_range_that_wraps_around_the_ring() { + assertThat( + range( + "127605887595351923798765477786913079296", + "85070591730234615865843651857942052864") + .splitEvenly(3)) + .containsExactly( + range("127605887595351923798765477786913079296", "0"), + range("0", "42535295865117307932921825928971026432"), + range( + "42535295865117307932921825928971026432", + "85070591730234615865843651857942052864")); + } + + @Test + public void should_split_range_producing_empty_splits_near_ring_end() { + // These are edge cases where we want to make sure we don't accidentally generate the ]min,min] + // range (which is the whole ring) + assertThat(range(MAX, MIN).splitEvenly(3)) + .containsExactly(range(MAX, MAX), range(MAX, MAX), range(MAX, MIN)); + + assertThat(range(MIN, "0").splitEvenly(3)) + .containsExactly(range(MIN, "0"), range("0", "0"), range("0", "0")); + } + + @Test + public void should_split_whole_ring() { + assertThat(range(MIN, MIN).splitEvenly(3)) + .containsExactly( + range(MIN, "56713727820156410577229101238628035242"), + range( + "56713727820156410577229101238628035242", + "113427455640312821154458202477256070485"), + range("113427455640312821154458202477256070485", MIN)); + } + + private RandomTokenRange range(String start, String end) { + return new RandomTokenRange( + new RandomToken(new BigInteger(start)), new RandomToken(new BigInteger(end))); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/SimpleReplicationStrategyTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/SimpleReplicationStrategyTest.java new file mode 100644 index 00000000000..7dd48a0088d --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/SimpleReplicationStrategyTest.java @@ -0,0 +1,215 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.SetMultimap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SimpleReplicationStrategyTest { + + private static final Token TOKEN01 = new Murmur3Token(-9000000000000000000L); + private static final Token TOKEN02 = new Murmur3Token(-8000000000000000000L); + private static final Token TOKEN03 = new Murmur3Token(-7000000000000000000L); + private static final Token TOKEN04 = new Murmur3Token(-6000000000000000000L); + private static final Token TOKEN05 = new Murmur3Token(-5000000000000000000L); + private static final Token TOKEN06 = new Murmur3Token(-4000000000000000000L); + private static final Token TOKEN07 = new Murmur3Token(-3000000000000000000L); + private static final Token TOKEN08 = new Murmur3Token(-2000000000000000000L); + private static final Token TOKEN09 = new Murmur3Token(-1000000000000000000L); + private static final Token TOKEN10 = new Murmur3Token(0L); + private static final Token TOKEN11 = new Murmur3Token(1000000000000000000L); + private static final Token TOKEN12 = new Murmur3Token(2000000000000000000L); + private static final Token TOKEN13 = new Murmur3Token(3000000000000000000L); + private static final Token TOKEN14 = new Murmur3Token(4000000000000000000L); + private static final Token TOKEN15 = new Murmur3Token(5000000000000000000L); + private static final Token TOKEN16 = new Murmur3Token(6000000000000000000L); + private static final Token TOKEN17 = new Murmur3Token(7000000000000000000L); + private static final Token TOKEN18 = new Murmur3Token(8000000000000000000L); + private static final Token TOKEN19 = new Murmur3Token(9000000000000000000L); + + @Mock private Node node1, node2, node3, node4, node5, node6; + + /** 4 tokens, 2 nodes, RF = 2. */ + @Test + public void should_compute_for_simple_layout() { + // Given + List ring = ImmutableList.of(TOKEN01, TOKEN06, TOKEN14, TOKEN19); + Map tokenToPrimary = + ImmutableMap.of(TOKEN01, node1, TOKEN06, node2, TOKEN14, node1, TOKEN19, node2); + SimpleReplicationStrategy strategy = new SimpleReplicationStrategy(2); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + // Note: this also asserts the iteration order of the sets (unlike containsEntry(token, set)) + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN06)).containsExactly(node2, node1); + assertThat(replicasByToken.get(TOKEN14)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN19)).containsExactly(node2, node1); + } + + /** 4 tokens, 2 nodes owning 2 consecutive tokens each, RF = 2. */ + @Test + public void should_compute_when_nodes_own_consecutive_tokens() { + // Given + List ring = ImmutableList.of(TOKEN01, TOKEN06, TOKEN14, TOKEN19); + Map tokenToPrimary = + ImmutableMap.of(TOKEN01, node1, TOKEN06, node1, TOKEN14, node2, TOKEN19, node2); + SimpleReplicationStrategy strategy = new SimpleReplicationStrategy(2); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN06)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN14)).containsExactly(node2, node1); + assertThat(replicasByToken.get(TOKEN19)).containsExactly(node2, node1); + } + + /** 4 tokens, 1 node owns 3 of them, RF = 2. */ + @Test + public void should_compute_when_ring_unbalanced() { + // Given + List ring = ImmutableList.of(TOKEN01, TOKEN06, TOKEN14, TOKEN19); + Map tokenToPrimary = + ImmutableMap.of(TOKEN01, node1, TOKEN06, node1, TOKEN14, node2, TOKEN19, node1); + SimpleReplicationStrategy strategy = new SimpleReplicationStrategy(2); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN06)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN14)).containsExactly(node2, node1); + assertThat(replicasByToken.get(TOKEN19)).containsExactly(node1, node2); + } + + /** 4 tokens, 2 nodes, RF = 6 (too large, should be <= number of nodes). */ + @Test + public void should_compute_when_replication_factor_is_larger_than_cluster_size() { + // Given + List ring = ImmutableList.of(TOKEN01, TOKEN06, TOKEN14, TOKEN19); + Map tokenToPrimary = + ImmutableMap.of(TOKEN01, node1, TOKEN06, node2, TOKEN14, node1, TOKEN19, node2); + SimpleReplicationStrategy strategy = new SimpleReplicationStrategy(6); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN06)).containsExactly(node2, node1); + assertThat(replicasByToken.get(TOKEN14)).containsExactly(node1, node2); + assertThat(replicasByToken.get(TOKEN19)).containsExactly(node2, node1); + } + + @Test + public void should_compute_for_complex_layout() { + // Given + List ring = + ImmutableList.builder() + .add(TOKEN01) + .add(TOKEN02) + .add(TOKEN03) + .add(TOKEN04) + .add(TOKEN05) + .add(TOKEN06) + .add(TOKEN07) + .add(TOKEN08) + .add(TOKEN09) + .add(TOKEN10) + .add(TOKEN11) + .add(TOKEN12) + .add(TOKEN13) + .add(TOKEN14) + .add(TOKEN15) + .add(TOKEN16) + .add(TOKEN17) + .add(TOKEN18) + .build(); + Map tokenToPrimary = + ImmutableMap.builder() + .put(TOKEN01, node1) + .put(TOKEN02, node1) + .put(TOKEN03, node5) + .put(TOKEN04, node3) + .put(TOKEN05, node1) + .put(TOKEN06, node5) + .put(TOKEN07, node2) + .put(TOKEN08, node6) + .put(TOKEN09, node3) + .put(TOKEN10, node4) + .put(TOKEN11, node5) + .put(TOKEN12, node4) + .put(TOKEN13, node4) + .put(TOKEN14, node2) + .put(TOKEN15, node6) + .put(TOKEN16, node3) + .put(TOKEN17, node2) + .put(TOKEN18, node6) + .build(); + + SimpleReplicationStrategy strategy = new SimpleReplicationStrategy(3); + + // When + SetMultimap replicasByToken = + strategy.computeReplicasByToken(tokenToPrimary, ring); + + // Then + assertThat(replicasByToken.keySet().size()).isEqualTo(ring.size()); + assertThat(replicasByToken.get(TOKEN01)).containsExactly(node1, node5, node3); + assertThat(replicasByToken.get(TOKEN02)).containsExactly(node1, node5, node3); + assertThat(replicasByToken.get(TOKEN03)).containsExactly(node5, node3, node1); + assertThat(replicasByToken.get(TOKEN04)).containsExactly(node3, node1, node5); + assertThat(replicasByToken.get(TOKEN05)).containsExactly(node1, node5, node2); + assertThat(replicasByToken.get(TOKEN06)).containsExactly(node5, node2, node6); + assertThat(replicasByToken.get(TOKEN07)).containsExactly(node2, node6, node3); + assertThat(replicasByToken.get(TOKEN08)).containsExactly(node6, node3, node4); + assertThat(replicasByToken.get(TOKEN09)).containsExactly(node3, node4, node5); + assertThat(replicasByToken.get(TOKEN10)).containsExactly(node4, node5, node2); + assertThat(replicasByToken.get(TOKEN11)).containsExactly(node5, node4, node2); + assertThat(replicasByToken.get(TOKEN12)).containsExactly(node4, node2, node6); + assertThat(replicasByToken.get(TOKEN13)).containsExactly(node4, node2, node6); + assertThat(replicasByToken.get(TOKEN14)).containsExactly(node2, node6, node3); + assertThat(replicasByToken.get(TOKEN15)).containsExactly(node6, node3, node2); + assertThat(replicasByToken.get(TOKEN16)).containsExactly(node3, node2, node6); + assertThat(replicasByToken.get(TOKEN17)).containsExactly(node2, node6, node1); + assertThat(replicasByToken.get(TOKEN18)).containsExactly(node6, node1, node5); + } +} diff --git a/driver-core/src/test/java/com/datastax/driver/core/TokenRangeAssert.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/TokenRangeAssert.java similarity index 81% rename from driver-core/src/test/java/com/datastax/driver/core/TokenRangeAssert.java rename to core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/TokenRangeAssert.java index 0c3647d6406..174fa69519a 100644 --- a/driver-core/src/test/java/com/datastax/driver/core/TokenRangeAssert.java +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/TokenRangeAssert.java @@ -13,16 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.datastax.driver.core; +package com.datastax.oss.driver.internal.core.metadata.token; -import static com.datastax.driver.core.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThat; -import java.util.Iterator; +import com.datastax.oss.driver.api.core.metadata.token.Token; +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; import java.util.List; import org.assertj.core.api.AbstractAssert; public class TokenRangeAssert extends AbstractAssert { - protected TokenRangeAssert(TokenRange actual) { + + public TokenRangeAssert(TokenRange actual) { super(actual, TokenRangeAssert.class); } @@ -49,20 +51,11 @@ public TokenRangeAssert isNotEmpty() { public TokenRangeAssert isWrappedAround() { assertThat(actual.isWrappedAround()).isTrue(); - Token.Factory factory = actual.factory; - List unwrapped = actual.unwrap(); assertThat(unwrapped.size()) .as("%s should unwrap to two ranges, but unwrapped to %s", actual, unwrapped) .isEqualTo(2); - Iterator unwrappedIt = unwrapped.iterator(); - TokenRange firstRange = unwrappedIt.next(); - assertThat(firstRange).endsWith(factory.minToken()); - - TokenRange secondRange = unwrappedIt.next(); - assertThat(secondRange).startsWith(factory.minToken()); - return this; } @@ -96,12 +89,12 @@ public TokenRangeAssert doesNotIntersect(TokenRange... that) { } public TokenRangeAssert contains(Token token, boolean isStart) { - assertThat(actual.contains(token, isStart)).isTrue(); + assertThat(((TokenRangeBase) actual).contains(actual, token, isStart)).isTrue(); return this; } public TokenRangeAssert doesNotContain(Token token, boolean isStart) { - assertThat(actual.contains(token, isStart)).isFalse(); + assertThat(((TokenRangeBase) actual).contains(actual, token, isStart)).isFalse(); return this; } } diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/TokenRangeTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/TokenRangeTest.java new file mode 100644 index 00000000000..42111947ec2 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/metadata/token/TokenRangeTest.java @@ -0,0 +1,284 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.metadata.token; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.junit.Assert.fail; + +import com.datastax.oss.driver.api.core.metadata.token.TokenRange; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; + +/** + * Covers the methods that don't depend on the underlying factory (we use Murmur3 as the + * implementation here). + * + * @see Murmur3TokenRangeTest + * @see ByteOrderedTokenRangeTest + * @see RandomTokenRangeTest + */ +public class TokenRangeTest { + + private Murmur3Token min = Murmur3TokenFactory.MIN_TOKEN; + + @Test + public void should_check_intersection() { + // NB - to make the test more visual, we use watch face numbers + assertThat(range(3, 9)) + .doesNotIntersect(range(11, 1)) + .doesNotIntersect(range(1, 2)) + .doesNotIntersect(range(11, 3)) + .doesNotIntersect(range(2, 3)) + .doesNotIntersect(range(3, 3)) + .intersects(range(2, 6)) + .intersects(range(2, 10)) + .intersects(range(6, 10)) + .intersects(range(4, 8)) + .intersects(range(3, 9)) + .doesNotIntersect(range(9, 10)) + .doesNotIntersect(range(10, 11)); + assertThat(range(9, 3)) + .doesNotIntersect(range(5, 7)) + .doesNotIntersect(range(7, 8)) + .doesNotIntersect(range(5, 9)) + .doesNotIntersect(range(8, 9)) + .doesNotIntersect(range(9, 9)) + .intersects(range(8, 2)) + .intersects(range(8, 4)) + .intersects(range(2, 4)) + .intersects(range(10, 2)) + .intersects(range(9, 3)) + .doesNotIntersect(range(3, 4)) + .doesNotIntersect(range(4, 5)); + assertThat(range(3, 3)).doesNotIntersect(range(3, 3)); + + // Reminder: minToken serves as both lower and upper bound + assertThat(minTo(5)) + .doesNotIntersect(range(6, 7)) + .doesNotIntersect(toMax(6)) + .intersects(range(6, 4)) + .intersects(range(2, 4)) + .intersects(minTo(4)) + .intersects(minTo(5)); + + assertThat(toMax(5)) + .doesNotIntersect(range(3, 4)) + .doesNotIntersect(minTo(4)) + .intersects(range(6, 7)) + .intersects(range(4, 1)) + .intersects(toMax(6)) + .intersects(toMax(5)); + + assertThat(fullRing()) + .intersects(range(3, 4)) + .intersects(toMax(3)) + .intersects(minTo(3)) + .doesNotIntersect(range(3, 3)); + } + + @Test + public void should_compute_intersection() { + assertThat(range(3, 9).intersectWith(range(2, 4))).isEqualTo(ImmutableList.of(range(3, 4))); + assertThat(range(3, 9).intersectWith(range(3, 5))).isEqualTo(ImmutableList.of(range(3, 5))); + assertThat(range(3, 9).intersectWith(range(4, 6))).isEqualTo(ImmutableList.of(range(4, 6))); + assertThat(range(3, 9).intersectWith(range(7, 9))).isEqualTo(ImmutableList.of(range(7, 9))); + assertThat(range(3, 9).intersectWith(range(8, 10))).isEqualTo(ImmutableList.of(range(8, 9))); + } + + @Test + public void should_compute_intersection_with_ranges_around_ring() { + // If a range wraps the ring (like 10, -10 does) this will produce two separate intersected + // ranges. + assertThat(range(10, -10).intersectWith(range(-20, 20))) + .isEqualTo(ImmutableList.of(range(10, 20), range(-20, -10))); + assertThat(range(-20, 20).intersectWith(range(10, -10))) + .isEqualTo(ImmutableList.of(range(10, 20), range(-20, -10))); + + // If both ranges wrap the ring, they should be merged together wrapping across the range. + assertThat(range(10, -30).intersectWith(range(20, -20))) + .isEqualTo(ImmutableList.of(range(20, -30))); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_compute_intersection_when_ranges_dont_intersect() { + range(1, 2).intersectWith(range(2, 3)); + } + + @Test + public void should_merge_with_other_range() { + assertThat(range(3, 9).mergeWith(range(2, 3))).isEqualTo(range(2, 9)); + assertThat(range(3, 9).mergeWith(range(2, 4))).isEqualTo(range(2, 9)); + assertThat(range(3, 9).mergeWith(range(11, 3))).isEqualTo(range(11, 9)); + assertThat(range(3, 9).mergeWith(range(11, 4))).isEqualTo(range(11, 9)); + + assertThat(range(3, 9).mergeWith(range(4, 8))).isEqualTo(range(3, 9)); + assertThat(range(3, 9).mergeWith(range(3, 9))).isEqualTo(range(3, 9)); + assertThat(range(3, 9).mergeWith(range(3, 3))).isEqualTo(range(3, 9)); + assertThat(range(3, 3).mergeWith(range(3, 9))).isEqualTo(range(3, 9)); + + assertThat(range(3, 9).mergeWith(range(9, 11))).isEqualTo(range(3, 11)); + assertThat(range(3, 9).mergeWith(range(8, 11))).isEqualTo(range(3, 11)); + assertThat(range(3, 9).mergeWith(range(9, 1))).isEqualTo(range(3, 1)); + assertThat(range(3, 9).mergeWith(range(8, 1))).isEqualTo(range(3, 1)); + + assertThat(range(3, 9).mergeWith(range(9, 3))).isEqualTo(fullRing()); + assertThat(range(3, 9).mergeWith(range(9, 4))).isEqualTo(fullRing()); + assertThat(range(3, 10).mergeWith(range(9, 4))).isEqualTo(fullRing()); + + assertThat(range(9, 3).mergeWith(range(8, 9))).isEqualTo(range(8, 3)); + assertThat(range(9, 3).mergeWith(range(8, 10))).isEqualTo(range(8, 3)); + assertThat(range(9, 3).mergeWith(range(4, 9))).isEqualTo(range(4, 3)); + assertThat(range(9, 3).mergeWith(range(4, 10))).isEqualTo(range(4, 3)); + + assertThat(range(9, 3).mergeWith(range(10, 2))).isEqualTo(range(9, 3)); + assertThat(range(9, 3).mergeWith(range(9, 3))).isEqualTo(range(9, 3)); + assertThat(range(9, 3).mergeWith(range(9, 9))).isEqualTo(range(9, 3)); + assertThat(range(9, 9).mergeWith(range(9, 3))).isEqualTo(range(9, 3)); + + assertThat(range(9, 3).mergeWith(range(3, 5))).isEqualTo(range(9, 5)); + assertThat(range(9, 3).mergeWith(range(2, 5))).isEqualTo(range(9, 5)); + assertThat(range(9, 3).mergeWith(range(3, 7))).isEqualTo(range(9, 7)); + assertThat(range(9, 3).mergeWith(range(2, 7))).isEqualTo(range(9, 7)); + + assertThat(range(9, 3).mergeWith(range(3, 9))).isEqualTo(fullRing()); + assertThat(range(9, 3).mergeWith(range(3, 10))).isEqualTo(fullRing()); + + assertThat(range(3, 3).mergeWith(range(3, 3))).isEqualTo(range(3, 3)); + + assertThat(toMax(5).mergeWith(range(6, 7))).isEqualTo(toMax(5)); + assertThat(toMax(5).mergeWith(minTo(3))).isEqualTo(range(5, 3)); + assertThat(toMax(5).mergeWith(range(3, 5))).isEqualTo(toMax(3)); + + assertThat(minTo(5).mergeWith(range(2, 3))).isEqualTo(minTo(5)); + assertThat(minTo(5).mergeWith(toMax(7))).isEqualTo(range(7, 5)); + assertThat(minTo(5).mergeWith(range(5, 7))).isEqualTo(minTo(7)); + } + + @Test(expected = IllegalArgumentException.class) + public void should_not_merge_with_nonadjacent_and_disjoint_ranges() { + range(0, 5).mergeWith(range(7, 14)); + } + + @Test + public void should_return_non_empty_range_if_other_range_is_empty() { + assertThat(range(1, 5).mergeWith(range(5, 5))).isEqualTo(range(1, 5)); + } + + @Test + public void should_unwrap_to_non_wrapping_ranges() { + assertThat(range(9, 3)).unwrapsTo(toMax(9), minTo(3)); + assertThat(range(3, 9)).isNotWrappedAround(); + assertThat(toMax(3)).isNotWrappedAround(); + assertThat(minTo(3)).isNotWrappedAround(); + assertThat(range(3, 3)).isNotWrappedAround(); + assertThat(fullRing()).isNotWrappedAround(); + } + + @Test + public void should_split_evenly() { + // Simply exercise splitEvenly, split logic is exercised in the test of each TokenRange + // implementation + List splits = range(3, 9).splitEvenly(3); + + assertThat(splits).hasSize(3); + assertThat(splits).containsExactly(range(3, 5), range(5, 7), range(7, 9)); + } + + @Test + public void should_throw_error_with_less_than_1_splits() { + for (int i = -255; i < 1; i++) { + try { + range(0, 1).splitEvenly(i); + fail("Expected error when providing " + i + " splits."); + } catch (IllegalArgumentException e) { + // expected. + } + } + } + + @Test(expected = IllegalArgumentException.class) + public void should_not_split_empty_token_range() { + range(0, 0).splitEvenly(1); + } + + @Test + public void should_create_empty_token_ranges_if_too_many_splits() { + TokenRange range = range(0, 10); + + List ranges = range.splitEvenly(255); + assertThat(ranges).hasSize(255); + + for (int i = 0; i < ranges.size(); i++) { + TokenRange tr = ranges.get(i); + if (i < 10) { + assertThat(tr).isEqualTo(range(i, i + 1)); + } else { + assertThat(tr.isEmpty()); + } + } + } + + @Test + public void should_check_if_range_contains_token() { + // ]1,2] contains 2, but it does not contain the start of ]2,3] + assertThat(range(1, 2)) + .contains(new Murmur3Token(2), false) + .doesNotContain(new Murmur3Token(2), true); + // ]1,2] does not contain 1, but it contains the start of ]1,3] + assertThat(range(1, 2)) + .doesNotContain(new Murmur3Token(1), false) + .contains(new Murmur3Token(1), true); + + // ]2,1] contains the start of ]min,5] + assertThat(range(2, 1)).contains(min, true); + + // ]min, 1] does not contain min, but it contains the start of ]min, 2] + assertThat(minTo(1)).doesNotContain(min, false).contains(min, true); + // ]1, min] contains min, but not the start of ]min, 2] + assertThat(toMax(1)).contains(min, false).doesNotContain(min, true); + + // An empty range contains nothing + assertThat(range(1, 1)) + .doesNotContain(new Murmur3Token(1), true) + .doesNotContain(new Murmur3Token(1), false) + .doesNotContain(min, true) + .doesNotContain(min, false); + + // The whole ring contains everything + assertThat(fullRing()) + .contains(min, true) + .contains(min, false) + .contains(new Murmur3Token(1), true) + .contains(new Murmur3Token(1), false); + } + + private TokenRange range(long start, long end) { + return new Murmur3TokenRange(new Murmur3Token(start), new Murmur3Token(end)); + } + + private TokenRange minTo(long end) { + return new Murmur3TokenRange(min, new Murmur3Token(end)); + } + + private TokenRange toMax(long start) { + return new Murmur3TokenRange(new Murmur3Token(start), min); + } + + private TokenRange fullRing() { + return new Murmur3TokenRange(Murmur3TokenFactory.MIN_TOKEN, Murmur3TokenFactory.MIN_TOKEN); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/os/NativeTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/os/NativeTest.java new file mode 100644 index 00000000000..b34015f31aa --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/os/NativeTest.java @@ -0,0 +1,31 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.os; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +import org.junit.Test; + +public class NativeTest { + + /** Verifies that {@link Native#getCPU()} returns non-empty cpu architecture */ + @Test + public void should_return_cpu_if_call_is_available() { + if (Native.isPlatformAvailable()) { + assertThat(Native.getCPU()).isNotEmpty(); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolInitTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolInitTest.java new file mode 100644 index 00000000000..3acfeb3b65d --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolInitTest.java @@ -0,0 +1,192 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.pool; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.InvalidKeyspaceException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; +import com.datastax.oss.driver.internal.core.channel.ChannelEvent; +import com.datastax.oss.driver.internal.core.channel.ClusterNameMismatchException; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.MockChannelFactoryHelper; +import com.datastax.oss.driver.internal.core.metadata.TopologyEvent; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.junit.Test; +import org.mockito.InOrder; + +public class ChannelPoolInitTest extends ChannelPoolTestBase { + + @Test + public void should_initialize_when_all_channels_succeed() throws Exception { + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(3); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + DriverChannel channel3 = newMockDriverChannel(3); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node, channel1) + .success(node, channel2) + .success(node, channel3) + .build(); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 3); + waitForPendingAdminTasks(); + + assertThatStage(poolFuture) + .isSuccess(pool -> assertThat(pool.channels).containsOnly(channel1, channel2, channel3)); + verify(eventBus, times(3)).fire(ChannelEvent.channelOpened(node)); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_initialize_when_all_channels_fail() throws Exception { + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(3); + + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .failure(node, "mock channel init failure") + .failure(node, "mock channel init failure") + .failure(node, "mock channel init failure") + .build(); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 3); + waitForPendingAdminTasks(); + + assertThatStage(poolFuture).isSuccess(pool -> assertThat(pool.channels).isEmpty()); + verify(eventBus, never()).fire(ChannelEvent.channelOpened(node)); + verify(nodeMetricUpdater, times(3)) + .incrementCounter(DefaultNodeMetric.CONNECTION_INIT_ERRORS, null); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_indicate_when_keyspace_failed_on_all_channels() { + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(3); + + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .failure(node, new InvalidKeyspaceException("invalid keyspace")) + .failure(node, new InvalidKeyspaceException("invalid keyspace")) + .failure(node, new InvalidKeyspaceException("invalid keyspace")) + .build(); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 3); + waitForPendingAdminTasks(); + assertThatStage(poolFuture) + .isSuccess( + pool -> { + assertThat(pool.isInvalidKeyspace()).isTrue(); + verify(nodeMetricUpdater, times(3)) + .incrementCounter(DefaultNodeMetric.CONNECTION_INIT_ERRORS, null); + }); + } + + @Test + public void should_fire_force_down_event_when_cluster_name_does_not_match() throws Exception { + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(3); + + ClusterNameMismatchException error = + new ClusterNameMismatchException(node.getEndPoint(), "actual", "expected"); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .failure(node, error) + .failure(node, error) + .failure(node, error) + .build(); + + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 3); + waitForPendingAdminTasks(); + + verify(eventBus).fire(TopologyEvent.forceDown(node.getBroadcastRpcAddress().get())); + verify(eventBus, never()).fire(ChannelEvent.channelOpened(node)); + + verify(nodeMetricUpdater, times(3)) + .incrementCounter(DefaultNodeMetric.CONNECTION_INIT_ERRORS, null); + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_reconnect_when_init_incomplete() throws Exception { + // Short delay so we don't have to wait in the test + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + CompletableFuture channel2Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // Init: 1 channel fails, the other succeeds + .failure(node, "mock channel init failure") + .success(node, channel1) + // 1st reconnection + .pending(node, channel2Future) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1); + inOrder.verify(eventBus).fire(ChannelEvent.channelOpened(node)); + + // A reconnection should have been scheduled + verify(reconnectionSchedule).nextDelay(); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStarted(node)); + + channel2Future.complete(channel2); + factoryHelper.waitForCalls(node, 1); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelOpened(node)); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStopped(node)); + + assertThat(pool.channels).containsOnly(channel1, channel2); + + verify(nodeMetricUpdater).incrementCounter(DefaultNodeMetric.CONNECTION_INIT_ERRORS, null); + factoryHelper.verifyNoMoreCalls(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolKeyspaceTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolKeyspaceTest.java new file mode 100644 index 00000000000..a5a6e33c821 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolKeyspaceTest.java @@ -0,0 +1,121 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.pool; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.internal.core.channel.ChannelEvent; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.MockChannelFactoryHelper; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.junit.Test; + +public class ChannelPoolKeyspaceTest extends ChannelPoolTestBase { + + @Test + public void should_switch_keyspace_on_existing_channels() throws Exception { + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node, channel1) + .success(node, channel2) + .build(); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1, channel2); + + CqlIdentifier newKeyspace = CqlIdentifier.fromCql("new_keyspace"); + CompletionStage setKeyspaceFuture = pool.setKeyspace(newKeyspace); + waitForPendingAdminTasks(); + + verify(channel1).setKeyspace(newKeyspace); + verify(channel2).setKeyspace(newKeyspace); + + assertThatStage(setKeyspaceFuture).isSuccess(); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_switch_keyspace_on_pending_channels() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + + DriverChannel channel1 = newMockDriverChannel(1); + CompletableFuture channel1Future = new CompletableFuture<>(); + DriverChannel channel2 = newMockDriverChannel(2); + CompletableFuture channel2Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .failure(node, "mock channel init failure") + .failure(node, "mock channel init failure") + // reconnection + .pending(node, channel1Future) + .pending(node, channel2Future) + .build(); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + + // Check that reconnection has kicked in, but do not complete it yet + verify(reconnectionSchedule).nextDelay(); + verify(eventBus).fire(ChannelEvent.reconnectionStarted(node)); + factoryHelper.waitForCalls(node, 2); + + // Switch keyspace, it succeeds immediately since there is no active channel + CqlIdentifier newKeyspace = CqlIdentifier.fromCql("new_keyspace"); + CompletionStage setKeyspaceFuture = pool.setKeyspace(newKeyspace); + waitForPendingAdminTasks(); + assertThatStage(setKeyspaceFuture).isSuccess(); + + // Now let the two channels succeed to complete the reconnection + channel1Future.complete(channel1); + channel2Future.complete(channel2); + waitForPendingAdminTasks(); + + verify(eventBus).fire(ChannelEvent.reconnectionStopped(node)); + verify(channel1).setKeyspace(newKeyspace); + verify(channel2).setKeyspace(newKeyspace); + + factoryHelper.verifyNoMoreCalls(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolReconnectTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolReconnectTest.java new file mode 100644 index 00000000000..a932bfb4bea --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolReconnectTest.java @@ -0,0 +1,197 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.pool; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.internal.core.channel.ChannelEvent; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.MockChannelFactoryHelper; +import io.netty.channel.ChannelPromise; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.mockito.InOrder; + +public class ChannelPoolReconnectTest extends ChannelPoolTestBase { + + @Test + public void should_reconnect_when_channel_closes() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + DriverChannel channel3 = newMockDriverChannel(3); + CompletableFuture channel3Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node, channel1) + .success(node, channel2) + // reconnection + .pending(node, channel3Future) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1, channel2); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + + // Simulate fatal error on channel2 + ((ChannelPromise) channel2.closeFuture()) + .setFailure(new Exception("mock channel init failure")); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelClosed(node)); + + verify(reconnectionSchedule).nextDelay(); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStarted(node)); + factoryHelper.waitForCall(node); + + channel3Future.complete(channel3); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelOpened(node)); + verify(eventBus).fire(ChannelEvent.reconnectionStopped(node)); + + assertThat(pool.channels).containsOnly(channel1, channel3); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_reconnect_when_channel_starts_graceful_shutdown() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + DriverChannel channel3 = newMockDriverChannel(3); + CompletableFuture channel3Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node, channel1) + .success(node, channel2) + // reconnection + .pending(node, channel3Future) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1, channel2); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + + // Simulate graceful shutdown on channel2 + ((ChannelPromise) channel2.closeStartedFuture()).setSuccess(); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelClosed(node)); + + verify(reconnectionSchedule).nextDelay(); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStarted(node)); + factoryHelper.waitForCall(node); + + channel3Future.complete(channel3); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelOpened(node)); + verify(eventBus).fire(ChannelEvent.reconnectionStopped(node)); + + assertThat(pool.channels).containsOnly(channel1, channel3); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_let_current_attempt_complete_when_reconnecting_now() + throws ExecutionException, InterruptedException { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(1); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + CompletableFuture channel2Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node, channel1) + // reconnection + .pending(node, channel2Future) + .build(); + + InOrder inOrder = inOrder(eventBus); + + // Initial connection + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + factoryHelper.waitForCalls(node, 1); + waitForPendingAdminTasks(); + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + inOrder.verify(eventBus, times(1)).fire(ChannelEvent.channelOpened(node)); + + // Kill channel1, reconnection begins and starts initializing channel2, but the initialization + // is still pending (channel2Future not completed) + ((ChannelPromise) channel1.closeStartedFuture()).setSuccess(); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelClosed(node)); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStarted(node)); + verify(reconnectionSchedule).nextDelay(); + factoryHelper.waitForCalls(node, 1); + + // Force a reconnection, should not try to create a new channel since we have a pending one + pool.reconnectNow(); + waitForPendingAdminTasks(); + factoryHelper.verifyNoMoreCalls(); + inOrder.verify(eventBus, never()).fire(any()); + + // Complete the initialization of channel2, reconnection succeeds + channel2Future.complete(channel2); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelOpened(node)); + verify(eventBus).fire(ChannelEvent.reconnectionStopped(node)); + + assertThat(pool.channels).containsOnly(channel2); + + factoryHelper.verifyNoMoreCalls(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolResizeTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolResizeTest.java new file mode 100644 index 00000000000..57e5cf145eb --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolResizeTest.java @@ -0,0 +1,423 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.pool; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.internal.core.channel.ChannelEvent; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.MockChannelFactoryHelper; +import com.datastax.oss.driver.internal.core.config.ConfigChangeEvent; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.junit.Test; +import org.mockito.InOrder; + +public class ChannelPoolResizeTest extends ChannelPoolTestBase { + + @Test + public void should_shrink_outside_of_reconnection() throws Exception { + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_REMOTE_SIZE)).thenReturn(4); + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + DriverChannel channel3 = newMockDriverChannel(3); + DriverChannel channel4 = newMockDriverChannel(4); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node, channel1) + .success(node, channel2) + .success(node, channel3) + .success(node, channel4) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.REMOTE, context, "test"); + + factoryHelper.waitForCalls(node, 4); + waitForPendingAdminTasks(); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1, channel2, channel3, channel4); + inOrder.verify(eventBus, times(4)).fire(ChannelEvent.channelOpened(node)); + + pool.resize(NodeDistance.LOCAL); + + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelClosed(node)); + + assertThat(pool.channels).containsOnly(channel3, channel4); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_shrink_during_reconnection() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_REMOTE_SIZE)).thenReturn(4); + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + DriverChannel channel3 = newMockDriverChannel(3); + CompletableFuture channel3Future = new CompletableFuture<>(); + DriverChannel channel4 = newMockDriverChannel(4); + CompletableFuture channel4Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node, channel1) + .success(node, channel2) + .failure(node, "mock channel init failure") + .failure(node, "mock channel init failure") + // reconnection + .pending(node, channel3Future) + .pending(node, channel4Future) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.REMOTE, context, "test"); + + factoryHelper.waitForCalls(node, 4); + waitForPendingAdminTasks(); + + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1, channel2); + + // A reconnection should have been scheduled to add the missing channels, don't complete yet + verify(reconnectionSchedule).nextDelay(); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStarted(node)); + + pool.resize(NodeDistance.LOCAL); + + waitForPendingAdminTasks(); + + // Now allow the reconnected channels to complete initialization + channel3Future.complete(channel3); + channel4Future.complete(channel4); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + + // Pool should have shrinked back to 2. We keep the most recent channels so 1 and 2 get closed. + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelClosed(node)); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStopped(node)); + assertThat(pool.channels).containsOnly(channel3, channel4); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_grow_outside_of_reconnection() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_REMOTE_SIZE)).thenReturn(4); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + DriverChannel channel3 = newMockDriverChannel(3); + DriverChannel channel4 = newMockDriverChannel(4); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node, channel1) + .success(node, channel2) + // growth attempt + .success(node, channel3) + .success(node, channel4) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1, channel2); + + pool.resize(NodeDistance.REMOTE); + waitForPendingAdminTasks(); + + // The resizing should have triggered a reconnection + verify(reconnectionSchedule).nextDelay(); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStarted(node)); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStopped(node)); + + assertThat(pool.channels).containsOnly(channel1, channel2, channel3, channel4); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_grow_during_reconnection() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_REMOTE_SIZE)).thenReturn(4); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + CompletableFuture channel2Future = new CompletableFuture<>(); + DriverChannel channel3 = newMockDriverChannel(3); + CompletableFuture channel3Future = new CompletableFuture<>(); + DriverChannel channel4 = newMockDriverChannel(4); + CompletableFuture channel4Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node, channel1) + .failure(node, "mock channel init failure") + // first reconnection attempt + .pending(node, channel2Future) + // extra reconnection attempt after we realize the pool must grow + .pending(node, channel3Future) + .pending(node, channel4Future) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelOpened(node)); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1); + + // A reconnection should have been scheduled to add the missing channel, don't complete yet + verify(reconnectionSchedule).nextDelay(); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStarted(node)); + + pool.resize(NodeDistance.REMOTE); + + waitForPendingAdminTasks(); + + // Complete the channel for the first reconnection, bringing the count to 2 + channel2Future.complete(channel2); + factoryHelper.waitForCall(node); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelOpened(node)); + + assertThat(pool.channels).containsOnly(channel1, channel2); + + // A second attempt should have been scheduled since we're now still under the target size + verify(reconnectionSchedule, times(2)).nextDelay(); + // Same reconnection is still running, no additional events + inOrder.verify(eventBus, never()).fire(ChannelEvent.reconnectionStopped(node)); + inOrder.verify(eventBus, never()).fire(ChannelEvent.reconnectionStarted(node)); + + // Two more channels get opened, bringing us to the target count + factoryHelper.waitForCalls(node, 2); + channel3Future.complete(channel3); + channel4Future.complete(channel4); + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStopped(node)); + + assertThat(pool.channels).containsOnly(channel1, channel2, channel3, channel4); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_resize_outside_of_reconnection_if_config_changes() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + DriverChannel channel3 = newMockDriverChannel(3); + DriverChannel channel4 = newMockDriverChannel(4); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node, channel1) + .success(node, channel2) + // growth attempt + .success(node, channel3) + .success(node, channel4) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1, channel2); + + // Simulate a configuration change + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(4); + eventBus.fire(ConfigChangeEvent.INSTANCE); + waitForPendingAdminTasks(); + + // It should have triggered a reconnection + verify(reconnectionSchedule).nextDelay(); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStarted(node)); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStopped(node)); + + assertThat(pool.channels).containsOnly(channel1, channel2, channel3, channel4); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_resize_during_reconnection_if_config_changes() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + CompletableFuture channel2Future = new CompletableFuture<>(); + DriverChannel channel3 = newMockDriverChannel(3); + CompletableFuture channel3Future = new CompletableFuture<>(); + DriverChannel channel4 = newMockDriverChannel(4); + CompletableFuture channel4Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node, channel1) + .failure(node, "mock channel init failure") + // first reconnection attempt + .pending(node, channel2Future) + // extra reconnection attempt after we realize the pool must grow + .pending(node, channel3Future) + .pending(node, channel4Future) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelOpened(node)); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1); + + // A reconnection should have been scheduled to add the missing channel, don't complete yet + verify(reconnectionSchedule).nextDelay(); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStarted(node)); + + // Simulate a configuration change + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(4); + eventBus.fire(ConfigChangeEvent.INSTANCE); + waitForPendingAdminTasks(); + + // Complete the channel for the first reconnection, bringing the count to 2 + channel2Future.complete(channel2); + factoryHelper.waitForCall(node); + waitForPendingAdminTasks(); + inOrder.verify(eventBus).fire(ChannelEvent.channelOpened(node)); + + assertThat(pool.channels).containsOnly(channel1, channel2); + + // A second attempt should have been scheduled since we're now still under the target size + verify(reconnectionSchedule, times(2)).nextDelay(); + // Same reconnection is still running, no additional events + inOrder.verify(eventBus, never()).fire(ChannelEvent.reconnectionStopped(node)); + inOrder.verify(eventBus, never()).fire(ChannelEvent.reconnectionStarted(node)); + + // Two more channels get opened, bringing us to the target count + factoryHelper.waitForCalls(node, 2); + channel3Future.complete(channel3); + channel4Future.complete(channel4); + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + inOrder.verify(eventBus).fire(ChannelEvent.reconnectionStopped(node)); + + assertThat(pool.channels).containsOnly(channel1, channel2, channel3, channel4); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_ignore_config_change_if_not_relevant() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(2); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + .success(node, channel1) + .success(node, channel2) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 2); + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelOpened(node)); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + assertThat(pool.channels).containsOnly(channel1, channel2); + + // Config changes, but not for our distance + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_REMOTE_SIZE)).thenReturn(1); + eventBus.fire(ConfigChangeEvent.INSTANCE); + waitForPendingAdminTasks(); + + // It should not have triggered a reconnection + verify(reconnectionSchedule, never()).nextDelay(); + + factoryHelper.verifyNoMoreCalls(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolShutdownTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolShutdownTest.java new file mode 100644 index 00000000000..3efb2147247 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolShutdownTest.java @@ -0,0 +1,180 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.pool; + +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.internal.core.channel.ChannelEvent; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.channel.MockChannelFactoryHelper; +import io.netty.channel.ChannelPromise; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.junit.Test; +import org.mockito.InOrder; + +public class ChannelPoolShutdownTest extends ChannelPoolTestBase { + + @Test + public void should_close_all_channels_when_closed() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(3); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + DriverChannel channel3 = newMockDriverChannel(3); + DriverChannel channel4 = newMockDriverChannel(4); + CompletableFuture channel4Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node, channel1) + .success(node, channel2) + .success(node, channel3) + // reconnection + .pending(node, channel4Future) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 3); + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(3)).fire(ChannelEvent.channelOpened(node)); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + + // Simulate graceful shutdown on channel3 + ((ChannelPromise) channel3.closeStartedFuture()).setSuccess(); + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(1)).fire(ChannelEvent.channelClosed(node)); + + // Reconnection should have kicked in and started to open channel4, do not complete it yet + verify(reconnectionSchedule).nextDelay(); + factoryHelper.waitForCalls(node, 1); + + CompletionStage closeFuture = pool.closeAsync(); + waitForPendingAdminTasks(); + + // The two original channels were closed normally + verify(channel1).close(); + verify(channel2).close(); + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelClosed(node)); + // The closing channel was not closed again + verify(channel3, never()).close(); + + // Complete the reconnecting channel + channel4Future.complete(channel4); + waitForPendingAdminTasks(); + + // It should be force-closed once we find out the pool was closed + verify(channel4).forceClose(); + // No events because the channel was never really associated to the pool + inOrder.verify(eventBus, never()).fire(ChannelEvent.channelOpened(node)); + inOrder.verify(eventBus, never()).fire(ChannelEvent.channelClosed(node)); + + // We don't wait for reconnected channels to close, so the pool only depends on channel 1 to 3 + ((ChannelPromise) channel1.closeFuture()).setSuccess(); + ((ChannelPromise) channel2.closeFuture()).setSuccess(); + ((ChannelPromise) channel3.closeFuture()).setSuccess(); + + assertThatStage(closeFuture).isSuccess(); + + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_force_close_all_channels_when_force_closed() throws Exception { + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + + when(defaultProfile.getInt(DefaultDriverOption.CONNECTION_POOL_LOCAL_SIZE)).thenReturn(3); + + DriverChannel channel1 = newMockDriverChannel(1); + DriverChannel channel2 = newMockDriverChannel(2); + DriverChannel channel3 = newMockDriverChannel(3); + DriverChannel channel4 = newMockDriverChannel(4); + CompletableFuture channel4Future = new CompletableFuture<>(); + MockChannelFactoryHelper factoryHelper = + MockChannelFactoryHelper.builder(channelFactory) + // init + .success(node, channel1) + .success(node, channel2) + .success(node, channel3) + // reconnection + .pending(node, channel4Future) + .build(); + InOrder inOrder = inOrder(eventBus); + + CompletionStage poolFuture = + ChannelPool.init(node, null, NodeDistance.LOCAL, context, "test"); + + factoryHelper.waitForCalls(node, 3); + waitForPendingAdminTasks(); + + assertThatStage(poolFuture).isSuccess(); + ChannelPool pool = poolFuture.toCompletableFuture().get(); + inOrder.verify(eventBus, times(3)).fire(ChannelEvent.channelOpened(node)); + + // Simulate graceful shutdown on channel3 + ((ChannelPromise) channel3.closeStartedFuture()).setSuccess(); + waitForPendingAdminTasks(); + inOrder.verify(eventBus, times(1)).fire(ChannelEvent.channelClosed(node)); + + // Reconnection should have kicked in and started to open a channel, do not complete it yet + verify(reconnectionSchedule).nextDelay(); + factoryHelper.waitForCalls(node, 1); + + CompletionStage closeFuture = pool.forceCloseAsync(); + waitForPendingAdminTasks(); + + // The three original channels were force-closed + verify(channel1).forceClose(); + verify(channel2).forceClose(); + verify(channel3).forceClose(); + // Only two events because the one for channel3 was sent earlier + inOrder.verify(eventBus, times(2)).fire(ChannelEvent.channelClosed(node)); + + // Complete the reconnecting channel + channel4Future.complete(channel4); + waitForPendingAdminTasks(); + + // It should be force-closed once we find out the pool was closed + verify(channel4).forceClose(); + // No events because the channel was never really associated to the pool + inOrder.verify(eventBus, never()).fire(ChannelEvent.channelOpened(node)); + inOrder.verify(eventBus, never()).fire(ChannelEvent.channelClosed(node)); + + // We don't wait for reconnected channels to close, so the pool only depends on channel 1-3 + ((ChannelPromise) channel1.closeFuture()).setSuccess(); + ((ChannelPromise) channel2.closeFuture()).setSuccess(); + ((ChannelPromise) channel3.closeFuture()).setSuccess(); + + assertThatStage(closeFuture).isSuccess(); + + factoryHelper.verifyNoMoreCalls(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolTestBase.java new file mode 100644 index 00000000000..16164c950e3 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelPoolTestBase.java @@ -0,0 +1,127 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.pool; + +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.channel.ChannelFactory; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.metadata.TestNodeFactory; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.internal.core.metrics.NodeMetricUpdater; +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.Uninterruptibles; +import io.netty.channel.Channel; +import io.netty.channel.DefaultChannelPromise; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.channel.EventLoop; +import io.netty.util.concurrent.Future; +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.After; +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +abstract class ChannelPoolTestBase { + + @Mock protected InternalDriverContext context; + @Mock private DriverConfig config; + @Mock protected DriverExecutionProfile defaultProfile; + @Mock private ReconnectionPolicy reconnectionPolicy; + @Mock protected ReconnectionPolicy.ReconnectionSchedule reconnectionSchedule; + @Mock private NettyOptions nettyOptions; + @Mock protected ChannelFactory channelFactory; + @Mock protected MetricsFactory metricsFactory; + @Mock protected NodeMetricUpdater nodeMetricUpdater; + protected DefaultNode node; + protected EventBus eventBus; + private DefaultEventLoopGroup adminEventLoopGroup; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + adminEventLoopGroup = new DefaultEventLoopGroup(1); + + when(context.getNettyOptions()).thenReturn(nettyOptions); + when(nettyOptions.adminEventExecutorGroup()).thenReturn(adminEventLoopGroup); + when(context.getConfig()).thenReturn(config); + when(config.getDefaultProfile()).thenReturn(defaultProfile); + this.eventBus = spy(new EventBus("test")); + when(context.getEventBus()).thenReturn(eventBus); + when(context.getChannelFactory()).thenReturn(channelFactory); + + when(context.getReconnectionPolicy()).thenReturn(reconnectionPolicy); + when(reconnectionPolicy.newNodeSchedule(any(Node.class))).thenReturn(reconnectionSchedule); + // By default, set a large reconnection delay. Tests that care about reconnection will override + // it. + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofDays(1)); + + when(context.getMetricsFactory()).thenReturn(metricsFactory); + when(metricsFactory.newNodeUpdater(any(Node.class))).thenReturn(nodeMetricUpdater); + + node = TestNodeFactory.newNode(1, context); + } + + @After + public void teardown() { + adminEventLoopGroup.shutdownGracefully(100, 200, TimeUnit.MILLISECONDS); + } + + DriverChannel newMockDriverChannel(int id) { + DriverChannel driverChannel = mock(DriverChannel.class); + EventLoop adminExecutor = adminEventLoopGroup.next(); + Channel channel = mock(Channel.class); + DefaultChannelPromise closeFuture = new DefaultChannelPromise(channel, adminExecutor); + DefaultChannelPromise closeStartedFuture = new DefaultChannelPromise(channel, adminExecutor); + when(driverChannel.close()).thenReturn(closeFuture); + when(driverChannel.forceClose()).thenReturn(closeFuture); + when(driverChannel.closeFuture()).thenReturn(closeFuture); + when(driverChannel.closeStartedFuture()).thenReturn(closeStartedFuture); + when(driverChannel.setKeyspace(any(CqlIdentifier.class))) + .thenReturn(adminExecutor.newSucceededFuture(null)); + when(driverChannel.toString()).thenReturn("channel" + id); + return driverChannel; + } + + // Wait for all the tasks on the pool's admin executor to complete. + void waitForPendingAdminTasks() { + // This works because the event loop group is single-threaded + Future f = adminEventLoopGroup.schedule(() -> null, 5, TimeUnit.NANOSECONDS); + try { + Uninterruptibles.getUninterruptibly(f, 100, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + fail("unexpected error", e.getCause()); + } catch (TimeoutException e) { + fail("timed out while waiting for admin tasks to complete", e); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelSetTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelSetTest.java new file mode 100644 index 00000000000..5e1e12d13d8 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/pool/ChannelSetTest.java @@ -0,0 +1,115 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.pool; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ChannelSetTest { + @Mock private DriverChannel channel1, channel2, channel3; + private ChannelSet set; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + set = new ChannelSet(); + } + + @Test + public void should_return_null_when_empty() { + assertThat(set.size()).isEqualTo(0); + assertThat(set.next()).isNull(); + } + + @Test + public void should_return_element_when_single() { + // When + set.add(channel1); + + // Then + assertThat(set.size()).isEqualTo(1); + assertThat(set.next()).isEqualTo(channel1); + verify(channel1, never()).getAvailableIds(); + } + + @Test + public void should_return_most_available_when_multiple() { + // Given + when(channel1.getAvailableIds()).thenReturn(2); + when(channel2.getAvailableIds()).thenReturn(12); + when(channel3.getAvailableIds()).thenReturn(8); + + // When + set.add(channel1); + set.add(channel2); + set.add(channel3); + + // Then + assertThat(set.size()).isEqualTo(3); + assertThat(set.next()).isEqualTo(channel2); + verify(channel1).getAvailableIds(); + verify(channel2).getAvailableIds(); + verify(channel3).getAvailableIds(); + + // When + when(channel1.getAvailableIds()).thenReturn(15); + + // Then + assertThat(set.next()).isEqualTo(channel1); + } + + @Test + public void should_remove_channels() { + // Given + when(channel1.getAvailableIds()).thenReturn(2); + when(channel2.getAvailableIds()).thenReturn(12); + when(channel3.getAvailableIds()).thenReturn(8); + + set.add(channel1); + set.add(channel2); + set.add(channel3); + assertThat(set.next()).isEqualTo(channel2); + + // When + set.remove(channel2); + + // Then + assertThat(set.size()).isEqualTo(2); + assertThat(set.next()).isEqualTo(channel3); + + // When + set.remove(channel3); + + // Then + assertThat(set.size()).isEqualTo(1); + assertThat(set.next()).isEqualTo(channel1); + + // When + set.remove(channel1); + + // Then + assertThat(set.size()).isEqualTo(0); + assertThat(set.next()).isNull(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/protocol/ByteBufPrimitiveCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/protocol/ByteBufPrimitiveCodecTest.java new file mode 100644 index 00000000000..fadb80f871b --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/protocol/ByteBufPrimitiveCodecTest.java @@ -0,0 +1,354 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.protocol; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.internal.core.util.ByteBufs; +import com.datastax.oss.protocol.internal.util.Bytes; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * Note: we don't test trivial methods that simply delegate to ByteBuf, nor default implementations + * inherited from {@link com.datastax.oss.protocol.internal.PrimitiveCodec}. + */ +public class ByteBufPrimitiveCodecTest { + private ByteBufPrimitiveCodec codec = new ByteBufPrimitiveCodec(ByteBufAllocator.DEFAULT); + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void should_concatenate() { + ByteBuf left = ByteBufs.wrap(0xca, 0xfe); + ByteBuf right = ByteBufs.wrap(0xba, 0xbe); + assertThat(codec.concat(left, right)).containsExactly("0xcafebabe"); + } + + @Test + public void should_concatenate_slices() { + ByteBuf left = ByteBufs.wrap(0x00, 0xca, 0xfe, 0x00).slice(1, 2); + ByteBuf right = ByteBufs.wrap(0x00, 0x00, 0xba, 0xbe, 0x00).slice(2, 2); + + assertThat(codec.concat(left, right)).containsExactly("0xcafebabe"); + } + + @Test + public void should_read_inet_v4() { + ByteBuf source = + ByteBufs.wrap( + // length (as a byte) + 0x04, + // address + 0x7f, + 0x00, + 0x00, + 0x01, + // port (as an int) + 0x00, + 0x00, + 0x23, + 0x52); + InetSocketAddress inet = codec.readInet(source); + assertThat(inet.getAddress().getHostAddress()).isEqualTo("127.0.0.1"); + assertThat(inet.getPort()).isEqualTo(9042); + } + + @Test + public void should_read_inet_v6() { + ByteBuf lengthAndAddress = allocate(17); + lengthAndAddress.writeByte(16); + lengthAndAddress.writeLong(0); + lengthAndAddress.writeLong(1); + ByteBuf source = + codec.concat( + lengthAndAddress, + // port (as an int) + ByteBufs.wrap(0x00, 0x00, 0x23, 0x52)); + InetSocketAddress inet = codec.readInet(source); + assertThat(inet.getAddress().getHostAddress()).isEqualTo("0:0:0:0:0:0:0:1"); + assertThat(inet.getPort()).isEqualTo(9042); + } + + @Test + public void should_fail_to_read_inet_if_length_invalid() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Invalid address length: 3 ([127, 0, 1])"); + ByteBuf source = + ByteBufs.wrap( + // length (as a byte) + 0x03, + // address + 0x7f, + 0x00, + 0x01, + // port (as an int) + 0x00, + 0x00, + 0x23, + 0x52); + codec.readInet(source); + } + + @Test + public void should_read_inetaddr_v4() { + ByteBuf source = + ByteBufs.wrap( + // length (as a byte) + 0x04, + // address + 0x7f, + 0x00, + 0x00, + 0x01); + InetAddress inetAddr = codec.readInetAddr(source); + assertThat(inetAddr.getHostAddress()).isEqualTo("127.0.0.1"); + } + + @Test + public void should_read_inetaddr_v6() { + ByteBuf source = allocate(17); + source.writeByte(16); + source.writeLong(0); + source.writeLong(1); + InetAddress inetAddr = codec.readInetAddr(source); + assertThat(inetAddr.getHostAddress()).isEqualTo("0:0:0:0:0:0:0:1"); + } + + @Test + public void should_fail_to_read_inetaddr_if_length_invalid() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Invalid address length: 3 ([127, 0, 1])"); + + ByteBuf source = + ByteBufs.wrap( + // length (as a byte) + 0x03, + // address + 0x7f, + 0x00, + 0x01); + codec.readInetAddr(source); + } + + @Test + public void should_read_bytes() { + ByteBuf source = + ByteBufs.wrap( + // length (as an int) + 0x00, + 0x00, + 0x00, + 0x04, + // contents + 0xca, + 0xfe, + 0xba, + 0xbe); + ByteBuffer bytes = codec.readBytes(source); + assertThat(Bytes.toHexString(bytes)).isEqualTo("0xcafebabe"); + } + + @Test + public void should_read_null_bytes() { + ByteBuf source = ByteBufs.wrap(0xFF, 0xFF, 0xFF, 0xFF); // -1 (as an int) + assertThat(codec.readBytes(source)).isNull(); + } + + @Test + public void should_read_short_bytes() { + ByteBuf source = + ByteBufs.wrap( + // length (as an unsigned short) + 0x00, + 0x04, + // contents + 0xca, + 0xfe, + 0xba, + 0xbe); + assertThat(Bytes.toHexString(codec.readShortBytes(source))).isEqualTo("0xcafebabe"); + } + + @Test + public void should_read_string() { + ByteBuf source = + ByteBufs.wrap( + // length (as an unsigned short) + 0x00, + 0x05, + // UTF-8 contents + 0x68, + 0x65, + 0x6c, + 0x6c, + 0x6f); + assertThat(codec.readString(source)).isEqualTo("hello"); + } + + @Test + public void should_fail_to_read_string_if_not_enough_characters() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage( + "Not enough bytes to read an UTF-8 serialized string of size 4"); + + ByteBuf source = codec.allocate(2); + source.writeShort(4); + + codec.readString(source); + } + + @Test + public void should_read_long_string() { + ByteBuf source = + ByteBufs.wrap( + // length (as an int) + 0x00, + 0x00, + 0x00, + 0x05, + // UTF-8 contents + 0x68, + 0x65, + 0x6c, + 0x6c, + 0x6f); + assertThat(codec.readLongString(source)).isEqualTo("hello"); + } + + @Test + public void should_fail_to_read_long_string_if_not_enough_characters() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage( + "Not enough bytes to read an UTF-8 serialized string of size 4"); + ByteBuf source = codec.allocate(4); + source.writeInt(4); + + codec.readLongString(source); + } + + @Test + public void should_write_inet_v4() throws Exception { + ByteBuf dest = allocate(1 + 4 + 4); + InetSocketAddress inet = new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 9042); + codec.writeInet(inet, dest); + assertThat(dest) + .containsExactly( + "0x04" // size as a byte + + "7f000001" // address + + "00002352" // port + ); + } + + @Test + public void should_write_inet_v6() throws Exception { + ByteBuf dest = allocate(1 + 16 + 4); + InetSocketAddress inet = new InetSocketAddress(InetAddress.getByName("::1"), 9042); + codec.writeInet(inet, dest); + assertThat(dest) + .containsExactly( + "0x10" // size as a byte + + "00000000000000000000000000000001" // address + + "00002352" // port + ); + } + + @Test + public void should_write_inetaddr_v4() throws Exception { + ByteBuf dest = allocate(1 + 4); + InetAddress inetAddr = InetAddress.getByName("127.0.0.1"); + codec.writeInetAddr(inetAddr, dest); + assertThat(dest) + .containsExactly( + "0x04" // size as a byte + + "7f000001" // address + ); + } + + @Test + public void should_write_inetaddr_v6() throws Exception { + ByteBuf dest = allocate(1 + 16); + InetAddress inetAddr = InetAddress.getByName("::1"); + codec.writeInetAddr(inetAddr, dest); + assertThat(dest) + .containsExactly( + "0x10" // size as a byte + + "00000000000000000000000000000001" // address + ); + } + + @Test + public void should_write_string() { + ByteBuf dest = allocate(7); + codec.writeString("hello", dest); + assertThat(dest) + .containsExactly( + "0x0005" // size as an unsigned short + + "68656c6c6f" // UTF-8 contents + ); + } + + @Test + public void should_write_long_string() { + ByteBuf dest = allocate(9); + codec.writeLongString("hello", dest); + assertThat(dest) + .containsExactly( + "0x00000005" + + // size as an int + "68656c6c6f" // UTF-8 contents + ); + } + + @Test + public void should_write_bytes() { + ByteBuf dest = allocate(8); + codec.writeBytes(Bytes.fromHexString("0xcafebabe"), dest); + assertThat(dest) + .containsExactly( + "0x00000004" + + // size as an int + "cafebabe"); + } + + @Test + public void should_write_short_bytes() { + ByteBuf dest = allocate(6); + codec.writeShortBytes(new byte[] {(byte) 0xca, (byte) 0xfe, (byte) 0xba, (byte) 0xbe}, dest); + assertThat(dest) + .containsExactly( + "0x0004" + + // size as an unsigned short + "cafebabe"); + } + + @Test + public void should_write_null_bytes() { + ByteBuf dest = allocate(4); + codec.writeBytes((ByteBuffer) null, dest); + assertThat(dest).containsExactly("0xFFFFFFFF"); + } + + private static ByteBuf allocate(int length) { + return ByteBufAllocator.DEFAULT.buffer(length); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/protocol/FrameDecoderTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/protocol/FrameDecoderTest.java new file mode 100644 index 00000000000..c223cb15462 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/protocol/FrameDecoderTest.java @@ -0,0 +1,117 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.protocol; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.datastax.oss.driver.api.core.connection.FrameTooLongException; +import com.datastax.oss.driver.internal.core.channel.ChannelHandlerTestBase; +import com.datastax.oss.driver.internal.core.util.ByteBufs; +import com.datastax.oss.protocol.internal.Compressor; +import com.datastax.oss.protocol.internal.Frame; +import com.datastax.oss.protocol.internal.FrameCodec; +import com.datastax.oss.protocol.internal.response.AuthSuccess; +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import org.junit.Before; +import org.junit.Test; + +public class FrameDecoderTest extends ChannelHandlerTestBase { + // A valid binary payload for a response frame. + private static final ByteBuf VALID_PAYLOAD = + ByteBufs.fromHexString( + "0x84" // response frame, protocol version 4 + + "00" // flags (none) + + "002a" // stream id (42) + + "10" // opcode for AUTH_SUCCESS message + + "00000008" // body length + + "00000004cafebabe" // body + ); + + // A binary payload that is invalid because the protocol version is not supported by the codec + private static final ByteBuf INVALID_PAYLOAD = + ByteBufs.fromHexString( + "0xFF" // response frame, protocol version 127 + + "00002a100000000800000004cafebabe"); + + private FrameCodec frameCodec; + + @Before + @Override + public void setup() { + super.setup(); + frameCodec = + FrameCodec.defaultClient(new ByteBufPrimitiveCodec(channel.alloc()), Compressor.none()); + } + + @Test + public void should_decode_valid_payload() { + // Given + FrameDecoder decoder = new FrameDecoder(frameCodec, 1024); + channel.pipeline().addLast(decoder); + + // When + // The decoder releases the buffer, so make sure we retain it for the other tests + VALID_PAYLOAD.retain(); + channel.writeInbound(VALID_PAYLOAD.duplicate()); + Frame frame = readInboundFrame(); + + // Then + assertThat(frame.message).isInstanceOf(AuthSuccess.class); + } + + /** + * Checks that an exception carrying the stream id is thrown when decoding fails in the {@link + * LengthFieldBasedFrameDecoder} code. + */ + @Test + public void should_fail_to_decode_if_payload_is_valid_but_too_long() { + // Given + FrameDecoder decoder = new FrameDecoder(frameCodec, VALID_PAYLOAD.readableBytes() - 1); + channel.pipeline().addLast(decoder); + + // When + VALID_PAYLOAD.retain(); + try { + channel.writeInbound(VALID_PAYLOAD.duplicate()); + fail("expected an exception"); + } catch (FrameDecodingException e) { + // Then + assertThat(e.streamId).isEqualTo(42); + assertThat(e.getCause()).isInstanceOf(FrameTooLongException.class); + } + } + + /** Checks that an exception carrying the stream id is thrown when decoding fails in our code. */ + @Test + public void should_fail_to_decode_if_payload_cannot_be_decoded() { + // Given + FrameDecoder decoder = new FrameDecoder(frameCodec, 1024); + channel.pipeline().addLast(decoder); + + // When + INVALID_PAYLOAD.retain(); + try { + channel.writeInbound(INVALID_PAYLOAD.duplicate()); + fail("expected an exception"); + } catch (FrameDecodingException e) { + // Then + assertThat(e.streamId).isEqualTo(42); + assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/session/DefaultSessionPoolsTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/session/DefaultSessionPoolsTest.java new file mode 100644 index 00000000000..d4620c4b522 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/session/DefaultSessionPoolsTest.java @@ -0,0 +1,962 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.addresstranslation.AddressTranslator; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Metadata; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.retry.RetryPolicy; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import com.datastax.oss.driver.api.core.tracker.RequestTracker; +import com.datastax.oss.driver.internal.core.context.EventBus; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.control.ControlConnection; +import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; +import com.datastax.oss.driver.internal.core.metadata.DefaultNode; +import com.datastax.oss.driver.internal.core.metadata.DistanceEvent; +import com.datastax.oss.driver.internal.core.metadata.LoadBalancingPolicyWrapper; +import com.datastax.oss.driver.internal.core.metadata.MetadataManager; +import com.datastax.oss.driver.internal.core.metadata.NodeStateEvent; +import com.datastax.oss.driver.internal.core.metadata.TestNodeFactory; +import com.datastax.oss.driver.internal.core.metadata.TopologyMonitor; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.internal.core.pool.ChannelPool; +import com.datastax.oss.driver.internal.core.pool.ChannelPoolFactory; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.Uninterruptibles; +import io.netty.channel.DefaultEventLoopGroup; +import io.netty.util.concurrent.DefaultPromise; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.GlobalEventExecutor; +import java.time.Duration; +import java.util.Collections; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class DefaultSessionPoolsTest { + + private static final CqlIdentifier KEYSPACE = CqlIdentifier.fromInternal("ks"); + + @Mock private InternalDriverContext context; + @Mock private NettyOptions nettyOptions; + @Mock private ChannelPoolFactory channelPoolFactory; + @Mock private MetadataManager metadataManager; + @Mock private TopologyMonitor topologyMonitor; + @Mock private LoadBalancingPolicyWrapper loadBalancingPolicyWrapper; + @Mock private DriverConfigLoader configLoader; + @Mock private Metadata metadata; + @Mock private DriverConfig config; + @Mock private DriverExecutionProfile defaultProfile; + @Mock private ReconnectionPolicy reconnectionPolicy; + @Mock private RetryPolicy retryPolicy; + @Mock private SpeculativeExecutionPolicy speculativeExecutionPolicy; + @Mock private AddressTranslator addressTranslator; + @Mock private ControlConnection controlConnection; + @Mock private MetricsFactory metricsFactory; + @Mock private NodeStateListener nodeStateListener; + @Mock private SchemaChangeListener schemaChangeListener; + @Mock private RequestTracker requestTracker; + + private DefaultNode node1; + private DefaultNode node2; + private DefaultNode node3; + private DefaultEventLoopGroup adminEventLoopGroup; + private EventBus eventBus; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + adminEventLoopGroup = new DefaultEventLoopGroup(1); + when(nettyOptions.adminEventExecutorGroup()).thenReturn(adminEventLoopGroup); + when(context.getNettyOptions()).thenReturn(nettyOptions); + + // Config: + when(defaultProfile.getBoolean(DefaultDriverOption.REQUEST_WARN_IF_SET_KEYSPACE)) + .thenReturn(true); + when(defaultProfile.getBoolean(DefaultDriverOption.REPREPARE_ENABLED)).thenReturn(false); + when(defaultProfile.isDefined(DefaultDriverOption.PROTOCOL_VERSION)).thenReturn(true); + when(defaultProfile.getDuration(DefaultDriverOption.METADATA_TOPOLOGY_WINDOW)) + .thenReturn(Duration.ZERO); + when(defaultProfile.getInt(DefaultDriverOption.METADATA_TOPOLOGY_MAX_EVENTS)).thenReturn(1); + when(config.getDefaultProfile()).thenReturn(defaultProfile); + when(context.getConfig()).thenReturn(config); + + // Init sequence: + when(metadataManager.refreshNodes()).thenReturn(CompletableFuture.completedFuture(null)); + when(metadataManager.firstSchemaRefreshFuture()) + .thenReturn(CompletableFuture.completedFuture(null)); + when(context.getMetadataManager()).thenReturn(metadataManager); + + when(topologyMonitor.init()).thenReturn(CompletableFuture.completedFuture(null)); + when(context.getTopologyMonitor()).thenReturn(topologyMonitor); + + when(context.getLoadBalancingPolicyWrapper()).thenReturn(loadBalancingPolicyWrapper); + + when(context.getConfigLoader()).thenReturn(configLoader); + + when(context.getMetricsFactory()).thenReturn(metricsFactory); + + // Runtime behavior: + when(context.getSessionName()).thenReturn("test"); + + when(context.getChannelPoolFactory()).thenReturn(channelPoolFactory); + + eventBus = spy(new EventBus("test")); + when(context.getEventBus()).thenReturn(eventBus); + + node1 = mockLocalNode(1); + node2 = mockLocalNode(2); + node3 = mockLocalNode(3); + ImmutableMap nodes = + ImmutableMap.of( + node1.getHostId(), node1, + node2.getHostId(), node2, + node3.getHostId(), node3); + when(metadata.getNodes()).thenReturn(nodes); + when(metadataManager.getMetadata()).thenReturn(metadata); + + PoolManager poolManager = new PoolManager(context); + when(context.getPoolManager()).thenReturn(poolManager); + + // Shutdown sequence: + when(context.getReconnectionPolicy()).thenReturn(reconnectionPolicy); + when(context.getRetryPolicy(DriverExecutionProfile.DEFAULT_NAME)).thenReturn(retryPolicy); + when(context.getSpeculativeExecutionPolicies()) + .thenReturn( + ImmutableMap.of(DriverExecutionProfile.DEFAULT_NAME, speculativeExecutionPolicy)); + when(context.getAddressTranslator()).thenReturn(addressTranslator); + when(context.getNodeStateListener()).thenReturn(nodeStateListener); + when(context.getSchemaChangeListener()).thenReturn(schemaChangeListener); + when(context.getRequestTracker()).thenReturn(requestTracker); + + when(metadataManager.closeAsync()).thenReturn(CompletableFuture.completedFuture(null)); + when(metadataManager.forceCloseAsync()).thenReturn(CompletableFuture.completedFuture(null)); + + when(topologyMonitor.closeAsync()).thenReturn(CompletableFuture.completedFuture(null)); + when(topologyMonitor.forceCloseAsync()).thenReturn(CompletableFuture.completedFuture(null)); + + when(context.getControlConnection()).thenReturn(controlConnection); + when(controlConnection.closeAsync()).thenReturn(CompletableFuture.completedFuture(null)); + when(controlConnection.forceCloseAsync()).thenReturn(CompletableFuture.completedFuture(null)); + + DefaultPromise nettyCloseFuture = new DefaultPromise<>(GlobalEventExecutor.INSTANCE); + nettyCloseFuture.setSuccess(null); + when(nettyOptions.onClose()).thenAnswer(invocation -> nettyCloseFuture); + } + + @Test + public void should_initialize_pools_with_distances() { + when(node3.getDistance()).thenReturn(NodeDistance.REMOTE); + + CompletableFuture pool1Future = new CompletableFuture<>(); + CompletableFuture pool2Future = new CompletableFuture<>(); + CompletableFuture pool3Future = new CompletableFuture<>(); + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .pending(node1, KEYSPACE, NodeDistance.LOCAL, pool1Future) + .pending(node2, KEYSPACE, NodeDistance.LOCAL, pool2Future) + .pending(node3, KEYSPACE, NodeDistance.REMOTE, pool3Future) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.REMOTE); + waitForPendingAdminTasks(); + + assertThatStage(initFuture).isNotDone(); + + pool1Future.complete(pool1); + pool2Future.complete(pool2); + pool3Future.complete(pool3); + waitForPendingAdminTasks(); + + assertThatStage(initFuture) + .isSuccess( + session -> + assertThat(((DefaultSession) session).getPools()) + .containsValues(pool1, pool2, pool3)); + } + + @Test + public void should_not_connect_to_ignored_nodes() { + when(node2.getDistance()).thenReturn(NodeDistance.IGNORED); + + ChannelPool pool1 = mockPool(node1); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + // Initial connection + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture) + .isSuccess( + session -> + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3)); + } + + @Test + public void should_not_connect_to_forced_down_nodes() { + when(node2.getState()).thenReturn(NodeState.FORCED_DOWN); + + ChannelPool pool1 = mockPool(node1); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + // Initial connection + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture) + .isSuccess( + session -> + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3)); + } + + @Test + public void should_adjust_distance_if_changed_while_init() { + CompletableFuture pool1Future = new CompletableFuture<>(); + CompletableFuture pool2Future = new CompletableFuture<>(); + CompletableFuture pool3Future = new CompletableFuture<>(); + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .pending(node1, KEYSPACE, NodeDistance.LOCAL, pool1Future) + .pending(node2, KEYSPACE, NodeDistance.LOCAL, pool2Future) + .pending(node3, KEYSPACE, NodeDistance.LOCAL, pool3Future) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + + assertThatStage(initFuture).isNotDone(); + + // Distance changes while init still pending + eventBus.fire(new DistanceEvent(NodeDistance.REMOTE, node2)); + + pool1Future.complete(pool1); + pool2Future.complete(pool2); + pool3Future.complete(pool3); + waitForPendingAdminTasks(); + + verify(pool2).resize(NodeDistance.REMOTE); + + assertThatStage(initFuture) + .isSuccess( + session -> + assertThat(((DefaultSession) session).getPools()) + .containsValues(pool1, pool2, pool3)); + } + + @Test + public void should_remove_pool_if_ignored_while_init() { + CompletableFuture pool1Future = new CompletableFuture<>(); + CompletableFuture pool2Future = new CompletableFuture<>(); + CompletableFuture pool3Future = new CompletableFuture<>(); + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .pending(node1, KEYSPACE, NodeDistance.LOCAL, pool1Future) + .pending(node2, KEYSPACE, NodeDistance.LOCAL, pool2Future) + .pending(node3, KEYSPACE, NodeDistance.LOCAL, pool3Future) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + + assertThatStage(initFuture).isNotDone(); + + // Distance changes while init still pending + eventBus.fire(new DistanceEvent(NodeDistance.IGNORED, node2)); + + pool1Future.complete(pool1); + pool2Future.complete(pool2); + pool3Future.complete(pool3); + waitForPendingAdminTasks(); + + verify(pool2).closeAsync(); + + assertThatStage(initFuture) + .isSuccess( + session -> + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3)); + } + + @Test + public void should_remove_pool_if_forced_down_while_init() { + CompletableFuture pool1Future = new CompletableFuture<>(); + CompletableFuture pool2Future = new CompletableFuture<>(); + CompletableFuture pool3Future = new CompletableFuture<>(); + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .pending(node1, KEYSPACE, NodeDistance.LOCAL, pool1Future) + .pending(node2, KEYSPACE, NodeDistance.LOCAL, pool2Future) + .pending(node3, KEYSPACE, NodeDistance.LOCAL, pool3Future) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + + assertThatStage(initFuture).isNotDone(); + + // Forced down while init still pending + eventBus.fire(NodeStateEvent.changed(NodeState.UP, NodeState.FORCED_DOWN, node2)); + + pool1Future.complete(pool1); + pool2Future.complete(pool2); + pool3Future.complete(pool3); + waitForPendingAdminTasks(); + + verify(pool2).closeAsync(); + + assertThatStage(initFuture) + .isSuccess( + session -> + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3)); + } + + @Test + public void should_resize_pool_if_distance_changes() { + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node2, KEYSPACE, NodeDistance.LOCAL, pool2) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + + eventBus.fire(new DistanceEvent(NodeDistance.REMOTE, node2)); + verify(pool2, timeout(500)).resize(NodeDistance.REMOTE); + } + + @Test + public void should_remove_pool_if_node_becomes_ignored() { + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node2, KEYSPACE, NodeDistance.LOCAL, pool2) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + + eventBus.fire(new DistanceEvent(NodeDistance.IGNORED, node2)); + verify(pool2, timeout(500)).closeAsync(); + + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + } + + @Test + public void should_do_nothing_if_node_becomes_ignored_but_was_already_ignored() { + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node2, KEYSPACE, NodeDistance.LOCAL, pool2) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + + eventBus.fire(new DistanceEvent(NodeDistance.IGNORED, node2)); + verify(pool2, timeout(100)).closeAsync(); + + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + + // Fire the same event again, nothing should happen + eventBus.fire(new DistanceEvent(NodeDistance.IGNORED, node2)); + waitForPendingAdminTasks(); + factoryHelper.verifyNoMoreCalls(); + } + + @Test + public void should_recreate_pool_if_node_becomes_not_ignored() { + when(node2.getDistance()).thenReturn(NodeDistance.IGNORED); + + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + // Initial connection + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + // When node2 becomes not ignored + .success(node2, KEYSPACE, NodeDistance.LOCAL, pool2) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + + eventBus.fire(new DistanceEvent(NodeDistance.LOCAL, node2)); + + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool2, pool3); + } + + @Test + public void should_remove_pool_if_node_is_forced_down() { + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node2, KEYSPACE, NodeDistance.LOCAL, pool2) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + + eventBus.fire(NodeStateEvent.changed(NodeState.UP, NodeState.FORCED_DOWN, node2)); + verify(pool2, timeout(500)).closeAsync(); + + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + } + + @Test + public void should_recreate_pool_if_node_is_forced_back_up() { + when(node2.getState()).thenReturn(NodeState.FORCED_DOWN); + + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + // init + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + // when node2 comes back up + .success(node2, KEYSPACE, NodeDistance.LOCAL, pool2) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + + eventBus.fire(NodeStateEvent.changed(NodeState.FORCED_DOWN, NodeState.UP, node2)); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool2, pool3); + } + + @Test + public void should_not_recreate_pool_if_node_is_forced_back_up_but_ignored() { + when(node2.getState()).thenReturn(NodeState.FORCED_DOWN); + when(node2.getDistance()).thenReturn(NodeDistance.IGNORED); + + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + // init + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + + eventBus.fire(NodeStateEvent.changed(NodeState.FORCED_DOWN, NodeState.UP, node2)); + waitForPendingAdminTasks(); + factoryHelper.verifyNoMoreCalls(); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + } + + @Test + public void should_adjust_distance_if_changed_while_recreating() { + when(node2.getDistance()).thenReturn(NodeDistance.IGNORED); + + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + CompletableFuture pool2Future = new CompletableFuture<>(); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + // Initial connection + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + // When node2 becomes not ignored + .pending(node2, KEYSPACE, NodeDistance.LOCAL, pool2Future) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + + eventBus.fire(new DistanceEvent(NodeDistance.LOCAL, node2)); + + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + + // Distance changes again while pool init is in progress + eventBus.fire(new DistanceEvent(NodeDistance.REMOTE, node2)); + + // Now pool init succeeds + pool2Future.complete(pool2); + waitForPendingAdminTasks(); + + // Pool should have been adjusted + verify(pool2).resize(NodeDistance.REMOTE); + + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool2, pool3); + } + + @Test + public void should_remove_pool_if_ignored_while_recreating() { + when(node2.getDistance()).thenReturn(NodeDistance.IGNORED); + + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + CompletableFuture pool2Future = new CompletableFuture<>(); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + // Initial connection + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + // When node2 becomes not ignored + .pending(node2, KEYSPACE, NodeDistance.LOCAL, pool2Future) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + + eventBus.fire(new DistanceEvent(NodeDistance.LOCAL, node2)); + + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + + // Distance changes to ignored while pool init is in progress + eventBus.fire(new DistanceEvent(NodeDistance.IGNORED, node2)); + + // Now pool init succeeds + pool2Future.complete(pool2); + waitForPendingAdminTasks(); + + // Pool should have been closed + verify(pool2).closeAsync(); + + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + } + + @Test + public void should_remove_pool_if_forced_down_while_recreating() { + when(node2.getDistance()).thenReturn(NodeDistance.IGNORED); + + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + CompletableFuture pool2Future = new CompletableFuture<>(); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + // Initial connection + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + // When node2 becomes not ignored + .pending(node2, KEYSPACE, NodeDistance.LOCAL, pool2Future) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + + eventBus.fire(new DistanceEvent(NodeDistance.LOCAL, node2)); + + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + + // Forced down while pool init is in progress + eventBus.fire(NodeStateEvent.changed(NodeState.UP, NodeState.FORCED_DOWN, node2)); + + // Now pool init succeeds + pool2Future.complete(pool2); + waitForPendingAdminTasks(); + + // Pool should have been closed + verify(pool2).closeAsync(); + + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + } + + @Test + public void should_close_all_pools_when_closing() { + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node2, KEYSPACE, NodeDistance.LOCAL, pool2) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + + CompletionStage closeFuture = session.closeAsync(); + waitForPendingAdminTasks(); + assertThatStage(closeFuture).isSuccess(); + + verify(pool1).closeAsync(); + verify(pool2).closeAsync(); + verify(pool3).closeAsync(); + } + + @Test + public void should_force_close_all_pools_when_force_closing() { + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node2, KEYSPACE, NodeDistance.LOCAL, pool2) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + + CompletionStage closeFuture = session.forceCloseAsync(); + waitForPendingAdminTasks(); + assertThatStage(closeFuture).isSuccess(); + + verify(pool1).forceCloseAsync(); + verify(pool2).forceCloseAsync(); + verify(pool3).forceCloseAsync(); + } + + @Test + public void should_close_pool_if_recreated_while_closing() { + when(node2.getState()).thenReturn(NodeState.FORCED_DOWN); + + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + CompletableFuture pool2Future = new CompletableFuture<>(); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + // init + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + // when node2 comes back up + .pending(node2, KEYSPACE, NodeDistance.LOCAL, pool2Future) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(((DefaultSession) session).getPools()).containsValues(pool1, pool3); + + // node2 comes back up, start initializing a pool for it + eventBus.fire(NodeStateEvent.changed(NodeState.FORCED_DOWN, NodeState.UP, node2)); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + + // but the session gets closed before pool init completes + CompletionStage closeFuture = session.closeAsync(); + waitForPendingAdminTasks(); + assertThatStage(closeFuture).isSuccess(); + + // now pool init completes + pool2Future.complete(pool2); + waitForPendingAdminTasks(); + + // Pool should have been closed + verify(pool2).forceCloseAsync(); + } + + @Test + public void should_set_keyspace_on_all_pools() { + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node2, KEYSPACE, NodeDistance.LOCAL, pool2) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + Session session = CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + + CqlIdentifier newKeyspace = CqlIdentifier.fromInternal("newKeyspace"); + ((DefaultSession) session).setKeyspace(newKeyspace); + waitForPendingAdminTasks(); + + verify(pool1).setKeyspace(newKeyspace); + verify(pool2).setKeyspace(newKeyspace); + verify(pool3).setKeyspace(newKeyspace); + } + + @Test + public void should_set_keyspace_on_pool_if_recreated_while_switching_keyspace() { + when(node2.getState()).thenReturn(NodeState.FORCED_DOWN); + + ChannelPool pool1 = mockPool(node1); + ChannelPool pool2 = mockPool(node2); + CompletableFuture pool2Future = new CompletableFuture<>(); + ChannelPool pool3 = mockPool(node3); + MockChannelPoolFactoryHelper factoryHelper = + MockChannelPoolFactoryHelper.builder(channelPoolFactory) + // init + .success(node1, KEYSPACE, NodeDistance.LOCAL, pool1) + .success(node3, KEYSPACE, NodeDistance.LOCAL, pool3) + // when node2 comes back up + .pending(node2, KEYSPACE, NodeDistance.LOCAL, pool2Future) + .build(); + + CompletionStage initFuture = newSession(); + + factoryHelper.waitForCall(node1, KEYSPACE, NodeDistance.LOCAL); + factoryHelper.waitForCall(node3, KEYSPACE, NodeDistance.LOCAL); + waitForPendingAdminTasks(); + assertThatStage(initFuture).isSuccess(); + DefaultSession session = + (DefaultSession) CompletableFutures.getCompleted(initFuture.toCompletableFuture()); + assertThat(session.getPools()).containsValues(pool1, pool3); + + // node2 comes back up, start initializing a pool for it + eventBus.fire(NodeStateEvent.changed(NodeState.FORCED_DOWN, NodeState.UP, node2)); + factoryHelper.waitForCall(node2, KEYSPACE, NodeDistance.LOCAL); + + // Keyspace gets changed on the session in the meantime, node2's pool will miss it + CqlIdentifier newKeyspace = CqlIdentifier.fromInternal("newKeyspace"); + session.setKeyspace(newKeyspace); + waitForPendingAdminTasks(); + verify(pool1).setKeyspace(newKeyspace); + verify(pool3).setKeyspace(newKeyspace); + + // now pool init completes + pool2Future.complete(pool2); + waitForPendingAdminTasks(); + + // Pool should have been closed + verify(pool2).setKeyspace(newKeyspace); + } + + private ChannelPool mockPool(Node node) { + ChannelPool pool = mock(ChannelPool.class); + when(pool.getNode()).thenReturn(node); + when(pool.getInitialKeyspaceName()).thenReturn(KEYSPACE); + when(pool.setKeyspace(any(CqlIdentifier.class))) + .thenReturn(CompletableFuture.completedFuture(null)); + CompletableFuture closeFuture = new CompletableFuture<>(); + when(pool.closeFuture()).thenReturn(closeFuture); + when(pool.closeAsync()) + .then( + i -> { + closeFuture.complete(null); + return closeFuture; + }); + when(pool.forceCloseAsync()) + .then( + i -> { + closeFuture.complete(null); + return closeFuture; + }); + return pool; + } + + private CompletionStage newSession() { + return DefaultSession.init(context, Collections.emptySet(), KEYSPACE); + } + + private static DefaultNode mockLocalNode(int i) { + DefaultNode node = mock(DefaultNode.class); + when(node.getHostId()).thenReturn(UUID.randomUUID()); + DefaultEndPoint endPoint = TestNodeFactory.newEndPoint(i); + when(node.getEndPoint()).thenReturn(endPoint); + when(node.getBroadcastRpcAddress()).thenReturn(Optional.of(endPoint.resolve())); + when(node.getDistance()).thenReturn(NodeDistance.LOCAL); + when(node.toString()).thenReturn("node" + i); + return node; + } + + // Wait for all the tasks on the pool's admin executor to complete. + private void waitForPendingAdminTasks() { + // This works because the event loop group is single-threaded + Future f = adminEventLoopGroup.schedule(() -> null, 5, TimeUnit.NANOSECONDS); + try { + Uninterruptibles.getUninterruptibly(f, 100, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + fail("unexpected error", e.getCause()); + } catch (TimeoutException e) { + fail("timed out while waiting for admin tasks to complete", e); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/session/MockChannelPoolFactoryHelper.java b/core/src/test/java/com/datastax/oss/driver/internal/core/session/MockChannelPoolFactoryHelper.java new file mode 100644 index 00000000000..4aa7e414939 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/session/MockChannelPoolFactoryHelper.java @@ -0,0 +1,225 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.pool.ChannelPool; +import com.datastax.oss.driver.internal.core.pool.ChannelPoolFactory; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.collect.ListMultimap; +import com.datastax.oss.driver.shaded.guava.common.collect.MultimapBuilder; +import com.datastax.oss.driver.shaded.guava.common.collect.Sets; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.internal.util.MockUtil; +import org.mockito.stubbing.OngoingStubbing; + +public class MockChannelPoolFactoryHelper { + + public static MockChannelPoolFactoryHelper.Builder builder( + ChannelPoolFactory channelPoolFactory) { + return new MockChannelPoolFactoryHelper.Builder(channelPoolFactory); + } + + private final ChannelPoolFactory channelPoolFactory; + private final InOrder inOrder; + // If waitForCalls sees more invocations than expected, the difference is stored here + private final Map previous = new HashMap<>(); + + private MockChannelPoolFactoryHelper(ChannelPoolFactory channelPoolFactory) { + this.channelPoolFactory = channelPoolFactory; + this.inOrder = inOrder(channelPoolFactory); + } + + public void waitForCall(Node node, CqlIdentifier keyspace, NodeDistance distance) { + waitForCalls(node, keyspace, distance, 1); + } + + /** + * Waits for a given number of calls to {@code ChannelPoolFactory.init()}. + * + *

Because we test asynchronous, non-blocking code, there might already be more calls than + * expected when this method is called. If so, the extra calls are stored and stored and will be + * taken into account next time. + */ + public void waitForCalls(Node node, CqlIdentifier keyspace, NodeDistance distance, int expected) { + Params params = new Params(node, keyspace, distance); + int fromLastTime = previous.getOrDefault(params, 0); + if (fromLastTime >= expected) { + previous.put(params, fromLastTime - expected); + return; + } + expected -= fromLastTime; + + // Because we test asynchronous, non-blocking code, there might have been already more + // invocations than expected. Use `atLeast` and a captor to find out. + ArgumentCaptor contextCaptor = + ArgumentCaptor.forClass(InternalDriverContext.class); + inOrder + .verify(channelPoolFactory, timeout(500).atLeast(expected)) + .init(eq(node), eq(keyspace), eq(distance), contextCaptor.capture(), eq("test")); + int actual = contextCaptor.getAllValues().size(); + + int extras = actual - expected; + if (extras > 0) { + previous.compute(params, (k, v) -> (v == null) ? extras : v + extras); + } + } + + public void verifyNoMoreCalls() { + inOrder + .verify(channelPoolFactory, timeout(500).times(0)) + .init( + any(Node.class), + any(CqlIdentifier.class), + any(NodeDistance.class), + any(InternalDriverContext.class), + any(String.class)); + + Set counts = Sets.newHashSet(previous.values()); + if (!counts.isEmpty()) { + assertThat(counts).containsExactly(0); + } + } + + public static class Builder { + private final ChannelPoolFactory channelPoolFactory; + private final ListMultimap invocations = + MultimapBuilder.hashKeys().arrayListValues().build(); + + private Builder(ChannelPoolFactory channelPoolFactory) { + assertThat(MockUtil.isMock(channelPoolFactory)).as("expected a mock").isTrue(); + verifyZeroInteractions(channelPoolFactory); + this.channelPoolFactory = channelPoolFactory; + } + + public Builder success( + Node node, CqlIdentifier keyspaceName, NodeDistance distance, ChannelPool pool) { + invocations.put(new Params(node, keyspaceName, distance), pool); + return this; + } + + public Builder failure( + Node node, CqlIdentifier keyspaceName, NodeDistance distance, String error) { + invocations.put(new Params(node, keyspaceName, distance), new Exception(error)); + return this; + } + + public Builder failure( + Node node, CqlIdentifier keyspaceName, NodeDistance distance, Throwable error) { + invocations.put(new Params(node, keyspaceName, distance), error); + return this; + } + + public Builder pending( + Node node, + CqlIdentifier keyspaceName, + NodeDistance distance, + CompletionStage future) { + invocations.put(new Params(node, keyspaceName, distance), future); + return this; + } + + public MockChannelPoolFactoryHelper build() { + stub(); + return new MockChannelPoolFactoryHelper(channelPoolFactory); + } + + private void stub() { + for (Params params : invocations.keySet()) { + Deque> results = new ArrayDeque<>(); + for (Object object : invocations.get(params)) { + if (object instanceof ChannelPool) { + results.add(CompletableFuture.completedFuture(((ChannelPool) object))); + } else if (object instanceof Throwable) { + results.add(CompletableFutures.failedFuture(((Throwable) object))); + } else if (object instanceof CompletableFuture) { + @SuppressWarnings("unchecked") + CompletionStage future = (CompletionStage) object; + results.add(future); + } else { + fail("unexpected type: " + object.getClass()); + } + } + if (results.size() > 0) { + CompletionStage first = results.poll(); + OngoingStubbing> ongoingStubbing = + when(channelPoolFactory.init( + eq(params.node), + eq(params.keyspace), + eq(params.distance), + any(InternalDriverContext.class), + eq("test"))) + .thenReturn(first); + for (CompletionStage result : results) { + ongoingStubbing.thenReturn(result); + } + } + } + } + } + + private static class Params { + private final Node node; + private final CqlIdentifier keyspace; + private final NodeDistance distance; + + private Params(Node node, CqlIdentifier keyspace, NodeDistance distance) { + this.node = node; + this.keyspace = keyspace; + this.distance = distance; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof Params) { + Params that = (Params) other; + return Objects.equals(this.node, that.node) + && Objects.equals(this.keyspace, that.keyspace) + && Objects.equals(this.distance, that.distance); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(node, keyspace, distance); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/session/ReprepareOnUpTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/session/ReprepareOnUpTest.java new file mode 100644 index 00000000000..6bb875d1dbd --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/session/ReprepareOnUpTest.java @@ -0,0 +1,365 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.internal.core.adminrequest.AdminResult; +import com.datastax.oss.driver.internal.core.channel.DriverChannel; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.metadata.TopologyMonitor; +import com.datastax.oss.driver.internal.core.metrics.MetricsFactory; +import com.datastax.oss.driver.internal.core.metrics.SessionMetricUpdater; +import com.datastax.oss.driver.internal.core.pool.ChannelPool; +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.protocol.internal.Message; +import com.datastax.oss.protocol.internal.ProtocolConstants; +import com.datastax.oss.protocol.internal.request.Prepare; +import com.datastax.oss.protocol.internal.request.Query; +import com.datastax.oss.protocol.internal.response.result.ColumnSpec; +import com.datastax.oss.protocol.internal.response.result.DefaultRows; +import com.datastax.oss.protocol.internal.response.result.RawType; +import com.datastax.oss.protocol.internal.response.result.Rows; +import com.datastax.oss.protocol.internal.response.result.RowsMetadata; +import com.datastax.oss.protocol.internal.util.Bytes; +import io.netty.channel.EventLoop; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ReprepareOnUpTest { + @Mock private ChannelPool pool; + @Mock private DriverChannel channel; + @Mock private EventLoop eventLoop; + @Mock private InternalDriverContext context; + @Mock private DriverConfig config; + @Mock private DriverExecutionProfile defaultProfile; + @Mock private TopologyMonitor topologyMonitor; + @Mock private MetricsFactory metricsFactory; + @Mock private SessionMetricUpdater metricUpdater; + private Runnable whenPrepared; + private CompletionStage done; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(pool.next()).thenReturn(channel); + when(channel.eventLoop()).thenReturn(eventLoop); + when(eventLoop.inEventLoop()).thenReturn(true); + + when(config.getDefaultProfile()).thenReturn(defaultProfile); + when(defaultProfile.getBoolean(DefaultDriverOption.REPREPARE_CHECK_SYSTEM_TABLE)) + .thenReturn(true); + when(defaultProfile.getDuration(DefaultDriverOption.REPREPARE_TIMEOUT)) + .thenReturn(Duration.ofMillis(500)); + when(defaultProfile.getInt(DefaultDriverOption.REPREPARE_MAX_STATEMENTS)).thenReturn(0); + when(defaultProfile.getInt(DefaultDriverOption.REPREPARE_MAX_PARALLELISM)).thenReturn(100); + when(context.getConfig()).thenReturn(config); + + when(context.getMetricsFactory()).thenReturn(metricsFactory); + when(metricsFactory.getSessionUpdater()).thenReturn(metricUpdater); + + done = new CompletableFuture<>(); + whenPrepared = () -> ((CompletableFuture) done).complete(null); + } + + @Test + public void should_complete_immediately_if_no_prepared_statements() { + // Given + MockReprepareOnUp reprepareOnUp = + new MockReprepareOnUp("test", pool, getMockPayloads(/*none*/ ), context, whenPrepared); + + // When + reprepareOnUp.start(); + + // Then + assertThatStage(done).isSuccess(v -> assertThat(reprepareOnUp.queries).isEmpty()); + } + + @Test + public void should_complete_immediately_if_pool_empty() { + // Given + when(pool.next()).thenReturn(null); + MockReprepareOnUp reprepareOnUp = + new MockReprepareOnUp("test", pool, getMockPayloads('a'), context, whenPrepared); + + // When + reprepareOnUp.start(); + + // Then + assertThatStage(done).isSuccess(v -> assertThat(reprepareOnUp.queries).isEmpty()); + } + + @Test + public void should_reprepare_all_if_system_table_query_fails() { + MockReprepareOnUp reprepareOnUp = + new MockReprepareOnUp( + "test", pool, getMockPayloads('a', 'b', 'c', 'd', 'e', 'f'), context, whenPrepared); + + reprepareOnUp.start(); + + MockAdminQuery adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Query.class); + assertThat(((Query) adminQuery.request).query) + .isEqualTo("SELECT prepared_id FROM system.prepared_statements"); + adminQuery.resultFuture.completeExceptionally(new RuntimeException("mock error")); + + for (char c = 'a'; c <= 'f'; c++) { + adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Prepare.class); + assertThat(((Prepare) adminQuery.request).cqlQuery).isEqualTo("mock query " + c); + adminQuery.resultFuture.complete(null); + } + + assertThatStage(done).isSuccess(v -> assertThat(reprepareOnUp.queries).isEmpty()); + } + + @Test + public void should_reprepare_all_if_system_table_empty() { + MockReprepareOnUp reprepareOnUp = + new MockReprepareOnUp( + "test", pool, getMockPayloads('a', 'b', 'c', 'd', 'e', 'f'), context, whenPrepared); + + reprepareOnUp.start(); + + MockAdminQuery adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Query.class); + assertThat(((Query) adminQuery.request).query) + .isEqualTo("SELECT prepared_id FROM system.prepared_statements"); + // server knows no ids: + adminQuery.resultFuture.complete( + new AdminResult(preparedIdRows(/*none*/ ), null, DefaultProtocolVersion.DEFAULT)); + + for (char c = 'a'; c <= 'f'; c++) { + adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Prepare.class); + assertThat(((Prepare) adminQuery.request).cqlQuery).isEqualTo("mock query " + c); + adminQuery.resultFuture.complete(null); + } + + assertThatStage(done).isSuccess(v -> assertThat(reprepareOnUp.queries).isEmpty()); + } + + @Test + public void should_reprepare_all_if_system_query_disabled() { + when(defaultProfile.getBoolean(DefaultDriverOption.REPREPARE_CHECK_SYSTEM_TABLE)) + .thenReturn(false); + + MockReprepareOnUp reprepareOnUp = + new MockReprepareOnUp( + "test", pool, getMockPayloads('a', 'b', 'c', 'd', 'e', 'f'), context, whenPrepared); + + reprepareOnUp.start(); + + MockAdminQuery adminQuery; + for (char c = 'a'; c <= 'f'; c++) { + adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Prepare.class); + assertThat(((Prepare) adminQuery.request).cqlQuery).isEqualTo("mock query " + c); + adminQuery.resultFuture.complete(null); + } + + assertThatStage(done).isSuccess(v -> assertThat(reprepareOnUp.queries).isEmpty()); + } + + @Test + public void should_not_reprepare_already_known_statements() { + MockReprepareOnUp reprepareOnUp = + new MockReprepareOnUp( + "test", pool, getMockPayloads('a', 'b', 'c', 'd', 'e', 'f'), context, whenPrepared); + + reprepareOnUp.start(); + + MockAdminQuery adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Query.class); + assertThat(((Query) adminQuery.request).query) + .isEqualTo("SELECT prepared_id FROM system.prepared_statements"); + // server knows d, e and f already: + adminQuery.resultFuture.complete( + new AdminResult(preparedIdRows('d', 'e', 'f'), null, DefaultProtocolVersion.DEFAULT)); + + for (char c = 'a'; c <= 'c'; c++) { + adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Prepare.class); + assertThat(((Prepare) adminQuery.request).cqlQuery).isEqualTo("mock query " + c); + adminQuery.resultFuture.complete(null); + } + + assertThatStage(done).isSuccess(v -> assertThat(reprepareOnUp.queries).isEmpty()); + } + + @Test + public void should_proceed_if_schema_agreement_not_reached() { + when(topologyMonitor.checkSchemaAgreement()) + .thenReturn(CompletableFuture.completedFuture(false)); + should_not_reprepare_already_known_statements(); + } + + @Test + public void should_proceed_if_schema_agreement_fails() { + when(topologyMonitor.checkSchemaAgreement()) + .thenReturn(CompletableFutures.failedFuture(new RuntimeException("test"))); + should_not_reprepare_already_known_statements(); + } + + @Test + public void should_limit_number_of_statements_to_reprepare() { + when(defaultProfile.getInt(DefaultDriverOption.REPREPARE_MAX_STATEMENTS)).thenReturn(3); + + MockReprepareOnUp reprepareOnUp = + new MockReprepareOnUp( + "test", pool, getMockPayloads('a', 'b', 'c', 'd', 'e', 'f'), context, whenPrepared); + + reprepareOnUp.start(); + + MockAdminQuery adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Query.class); + assertThat(((Query) adminQuery.request).query) + .isEqualTo("SELECT prepared_id FROM system.prepared_statements"); + // server knows no ids: + adminQuery.resultFuture.complete( + new AdminResult(preparedIdRows(/*none*/ ), null, DefaultProtocolVersion.DEFAULT)); + + for (char c = 'a'; c <= 'c'; c++) { + adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Prepare.class); + assertThat(((Prepare) adminQuery.request).cqlQuery).isEqualTo("mock query " + c); + adminQuery.resultFuture.complete(null); + } + + assertThatStage(done).isSuccess(v -> assertThat(reprepareOnUp.queries).isEmpty()); + } + + @Test + public void should_limit_number_of_statements_reprepared_in_parallel() { + when(defaultProfile.getInt(DefaultDriverOption.REPREPARE_MAX_PARALLELISM)).thenReturn(3); + + MockReprepareOnUp reprepareOnUp = + new MockReprepareOnUp( + "test", pool, getMockPayloads('a', 'b', 'c', 'd', 'e', 'f'), context, whenPrepared); + + reprepareOnUp.start(); + + MockAdminQuery adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Query.class); + assertThat(((Query) adminQuery.request).query) + .isEqualTo("SELECT prepared_id FROM system.prepared_statements"); + // server knows no ids => will reprepare all 6: + adminQuery.resultFuture.complete( + new AdminResult(preparedIdRows(/*none*/ ), null, DefaultProtocolVersion.DEFAULT)); + + // 3 statements have enqueued, we've not completed the queries yet so no more should be sent: + assertThat(reprepareOnUp.queries.size()).isEqualTo(3); + + // As we complete each statement, another one should enqueue: + for (char c = 'a'; c <= 'c'; c++) { + adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Prepare.class); + assertThat(((Prepare) adminQuery.request).cqlQuery).isEqualTo("mock query " + c); + adminQuery.resultFuture.complete(null); + assertThat(reprepareOnUp.queries.size()).isEqualTo(3); + } + + // Complete the last 3: + for (char c = 'd'; c <= 'f'; c++) { + adminQuery = reprepareOnUp.queries.poll(); + assertThat(adminQuery.request).isInstanceOf(Prepare.class); + assertThat(((Prepare) adminQuery.request).cqlQuery).isEqualTo("mock query " + c); + adminQuery.resultFuture.complete(null); + } + + assertThatStage(done).isSuccess(v -> assertThat(reprepareOnUp.queries).isEmpty()); + } + + private Map getMockPayloads(char... values) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (char value : values) { + ByteBuffer id = Bytes.fromHexString("0x0" + value); + builder.put( + id, new RepreparePayload(id, "mock query " + value, null, Collections.emptyMap())); + } + return builder.build(); + } + + /** Bypasses the channel to make testing easier. */ + private static class MockReprepareOnUp extends ReprepareOnUp { + + private Queue queries = new ArrayDeque<>(); + + MockReprepareOnUp( + String logPrefix, + ChannelPool pool, + Map repreparePayloads, + InternalDriverContext context, + Runnable whenPrepared) { + super(logPrefix, pool, repreparePayloads, context, whenPrepared); + } + + @Override + protected CompletionStage queryAsync( + Message message, Map customPayload, String debugString) { + CompletableFuture resultFuture = new CompletableFuture<>(); + queries.add(new MockAdminQuery(message, resultFuture)); + return resultFuture; + } + } + + private static class MockAdminQuery { + private final Message request; + private final CompletableFuture resultFuture; + + public MockAdminQuery(Message request, CompletableFuture resultFuture) { + this.request = request; + this.resultFuture = resultFuture; + } + } + + private Rows preparedIdRows(char... values) { + ColumnSpec preparedIdSpec = + new ColumnSpec( + "system", + "prepared_statements", + "prepared_id", + 0, + RawType.PRIMITIVES.get(ProtocolConstants.DataType.BLOB)); + RowsMetadata rowsMetadata = + new RowsMetadata(ImmutableList.of(preparedIdSpec), null, null, null); + Queue> data = new ArrayDeque<>(); + for (char value : values) { + data.add(ImmutableList.of(Bytes.fromHexString("0x0" + value))); + } + return new DefaultRows(rowsMetadata, data); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/ConcurrencyLimitingRequestThrottlerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/ConcurrencyLimitingRequestThrottlerTest.java new file mode 100644 index 00000000000..a9f4233513b --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/ConcurrencyLimitingRequestThrottlerTest.java @@ -0,0 +1,240 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session.throttling; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.RequestThrottlingException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.session.throttling.Throttled; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import java.util.List; +import java.util.function.Consumer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ConcurrencyLimitingRequestThrottlerTest { + + @Mock private DriverContext context; + @Mock private DriverConfig config; + @Mock private DriverExecutionProfile defaultProfile; + + private ConcurrencyLimitingRequestThrottler throttler; + + @Before + public void setup() { + when(context.getConfig()).thenReturn(config); + when(config.getDefaultProfile()).thenReturn(defaultProfile); + + when(defaultProfile.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_CONCURRENT_REQUESTS)) + .thenReturn(5); + when(defaultProfile.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE)) + .thenReturn(10); + + throttler = new ConcurrencyLimitingRequestThrottler(context); + } + + @Test + public void should_start_immediately_when_under_capacity() { + // Given + MockThrottled request = new MockThrottled(); + + // When + throttler.register(request); + + // Then + assertThatStage(request.started).isSuccess(wasDelayed -> assertThat(wasDelayed).isFalse()); + assertThat(throttler.getConcurrentRequests()).isEqualTo(1); + assertThat(throttler.getQueue()).isEmpty(); + } + + @Test + public void should_allow_new_request_when_active_one_succeeds() { + should_allow_new_request_when_active_one_completes(throttler::signalSuccess); + } + + @Test + public void should_allow_new_request_when_active_one_fails() { + should_allow_new_request_when_active_one_completes( + request -> throttler.signalError(request, new RuntimeException("mock error"))); + } + + @Test + public void should_allow_new_request_when_active_one_times_out() { + should_allow_new_request_when_active_one_completes(throttler::signalTimeout); + } + + private void should_allow_new_request_when_active_one_completes( + Consumer completeCallback) { + // Given + MockThrottled first = new MockThrottled(); + throttler.register(first); + assertThatStage(first.started).isSuccess(wasDelayed -> assertThat(wasDelayed).isFalse()); + for (int i = 0; i < 4; i++) { // fill to capacity + throttler.register(new MockThrottled()); + } + assertThat(throttler.getConcurrentRequests()).isEqualTo(5); + assertThat(throttler.getQueue()).isEmpty(); + + // When + completeCallback.accept(first); + assertThat(throttler.getConcurrentRequests()).isEqualTo(4); + assertThat(throttler.getQueue()).isEmpty(); + MockThrottled incoming = new MockThrottled(); + throttler.register(incoming); + + // Then + assertThatStage(incoming.started).isSuccess(wasDelayed -> assertThat(wasDelayed).isFalse()); + assertThat(throttler.getConcurrentRequests()).isEqualTo(5); + assertThat(throttler.getQueue()).isEmpty(); + } + + @Test + public void should_enqueue_when_over_capacity() { + // Given + for (int i = 0; i < 5; i++) { + throttler.register(new MockThrottled()); + } + assertThat(throttler.getConcurrentRequests()).isEqualTo(5); + assertThat(throttler.getQueue()).isEmpty(); + + // When + MockThrottled incoming = new MockThrottled(); + throttler.register(incoming); + + // Then + assertThatStage(incoming.started).isNotDone(); + assertThat(throttler.getConcurrentRequests()).isEqualTo(5); + assertThat(throttler.getQueue()).containsExactly(incoming); + } + + @Test + public void should_dequeue_when_active_succeeds() { + should_dequeue_when_active_completes(throttler::signalSuccess); + } + + @Test + public void should_dequeue_when_active_fails() { + should_dequeue_when_active_completes( + request -> throttler.signalError(request, new RuntimeException("mock error"))); + } + + @Test + public void should_dequeue_when_active_times_out() { + should_dequeue_when_active_completes(throttler::signalTimeout); + } + + private void should_dequeue_when_active_completes(Consumer completeCallback) { + // Given + MockThrottled first = new MockThrottled(); + throttler.register(first); + assertThatStage(first.started).isSuccess(wasDelayed -> assertThat(wasDelayed).isFalse()); + for (int i = 0; i < 4; i++) { + throttler.register(new MockThrottled()); + } + + MockThrottled incoming = new MockThrottled(); + throttler.register(incoming); + assertThatStage(incoming.started).isNotDone(); + + // When + completeCallback.accept(first); + + // Then + assertThatStage(incoming.started).isSuccess(wasDelayed -> assertThat(wasDelayed).isTrue()); + assertThat(throttler.getConcurrentRequests()).isEqualTo(5); + assertThat(throttler.getQueue()).isEmpty(); + } + + @Test + public void should_reject_when_queue_is_full() { + // Given + for (int i = 0; i < 15; i++) { + throttler.register(new MockThrottled()); + } + assertThat(throttler.getConcurrentRequests()).isEqualTo(5); + assertThat(throttler.getQueue()).hasSize(10); + + // When + MockThrottled incoming = new MockThrottled(); + throttler.register(incoming); + + // Then + assertThatStage(incoming.started) + .isFailed(error -> assertThat(error).isInstanceOf(RequestThrottlingException.class)); + } + + @Test + public void should_remove_timed_out_request_from_queue() { + // Given + for (int i = 0; i < 5; i++) { + throttler.register(new MockThrottled()); + } + MockThrottled queued1 = new MockThrottled(); + throttler.register(queued1); + MockThrottled queued2 = new MockThrottled(); + throttler.register(queued2); + + // When + throttler.signalTimeout(queued1); + + // Then + assertThatStage(queued2.started).isNotDone(); + assertThat(throttler.getConcurrentRequests()).isEqualTo(5); + assertThat(throttler.getQueue()).hasSize(1); + } + + @Test + public void should_reject_enqueued_when_closing() { + // Given + for (int i = 0; i < 5; i++) { + throttler.register(new MockThrottled()); + } + List enqueued = Lists.newArrayList(); + for (int i = 0; i < 10; i++) { + MockThrottled request = new MockThrottled(); + throttler.register(request); + assertThatStage(request.started).isNotDone(); + enqueued.add(request); + } + + // When + throttler.close(); + + // Then + for (MockThrottled request : enqueued) { + assertThatStage(request.started) + .isFailed(error -> assertThat(error).isInstanceOf(RequestThrottlingException.class)); + } + + // When + MockThrottled request = new MockThrottled(); + throttler.register(request); + + // Then + assertThatStage(request.started) + .isFailed(error -> assertThat(error).isInstanceOf(RequestThrottlingException.class)); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/MockThrottled.java b/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/MockThrottled.java new file mode 100644 index 00000000000..ab723b150b0 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/MockThrottled.java @@ -0,0 +1,37 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session.throttling; + +import com.datastax.oss.driver.api.core.RequestThrottlingException; +import com.datastax.oss.driver.api.core.session.throttling.Throttled; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +class MockThrottled implements Throttled { + + final CompletionStage started = new CompletableFuture<>(); + + @Override + public void onThrottleReady(boolean wasDelayed) { + started.toCompletableFuture().complete(wasDelayed); + } + + @Override + public void onThrottleFailure(@NonNull RequestThrottlingException error) { + started.toCompletableFuture().completeExceptionally(error); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/RateLimitingRequestThrottlerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/RateLimitingRequestThrottlerTest.java new file mode 100644 index 00000000000..26b52403e8f --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/RateLimitingRequestThrottlerTest.java @@ -0,0 +1,317 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session.throttling; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.RequestThrottlingException; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.context.NettyOptions; +import com.datastax.oss.driver.internal.core.util.concurrent.ScheduledTaskCapturingEventLoop; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import io.netty.channel.EventLoopGroup; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.Silent.class) +public class RateLimitingRequestThrottlerTest { + + private static final long ONE_HUNDRED_MILLISECONDS = + TimeUnit.NANOSECONDS.convert(100, TimeUnit.MILLISECONDS); + private static final long TWO_HUNDRED_MILLISECONDS = + TimeUnit.NANOSECONDS.convert(200, TimeUnit.MILLISECONDS); + private static final long TWO_SECONDS = TimeUnit.NANOSECONDS.convert(2, TimeUnit.SECONDS); + + // Note: we trigger scheduled task manually, so this is for verification purposes only, it doesn't + // need to be consistent with the actual throttling rate. + private static final Duration DRAIN_INTERVAL = Duration.ofMillis(10); + + @Mock private InternalDriverContext context; + @Mock private DriverConfig config; + @Mock private DriverExecutionProfile defaultProfile; + @Mock private NettyOptions nettyOptions; + @Mock private EventLoopGroup adminGroup; + + private ScheduledTaskCapturingEventLoop adminExecutor; + private SettableNanoClock clock = new SettableNanoClock(); + + private RateLimitingRequestThrottler throttler; + + @Before + public void setup() { + when(context.getConfig()).thenReturn(config); + when(config.getDefaultProfile()).thenReturn(defaultProfile); + + when(defaultProfile.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_REQUESTS_PER_SECOND)) + .thenReturn(5); + when(defaultProfile.getInt(DefaultDriverOption.REQUEST_THROTTLER_MAX_QUEUE_SIZE)) + .thenReturn(10); + + // Set to match the time to reissue one permit. Although it does not matter in practice, since + // the executor is mocked and we trigger tasks manually. + when(defaultProfile.getDuration(DefaultDriverOption.REQUEST_THROTTLER_DRAIN_INTERVAL)) + .thenReturn(DRAIN_INTERVAL); + + when(context.getNettyOptions()).thenReturn(nettyOptions); + when(nettyOptions.adminEventExecutorGroup()).thenReturn(adminGroup); + adminExecutor = new ScheduledTaskCapturingEventLoop(adminGroup); + when(adminGroup.next()).thenReturn(adminExecutor); + + throttler = new RateLimitingRequestThrottler(context, clock); + } + + /** Note: the throttler starts with 1 second worth of permits, so at t=0 we have 5 available. */ + @Test + public void should_start_immediately_when_under_capacity() { + // Given + MockThrottled request = new MockThrottled(); + + // When + throttler.register(request); + + // Then + assertThatStage(request.started).isSuccess(wasDelayed -> assertThat(wasDelayed).isFalse()); + assertThat(throttler.getStoredPermits()).isEqualTo(4); + assertThat(throttler.getQueue()).isEmpty(); + } + + @Test + public void should_allow_new_request_when_under_rate() { + // Given + for (int i = 0; i < 5; i++) { + throttler.register(new MockThrottled()); + } + assertThat(throttler.getStoredPermits()).isEqualTo(0); + + // When + clock.add(TWO_HUNDRED_MILLISECONDS); + MockThrottled request = new MockThrottled(); + throttler.register(request); + + // Then + assertThatStage(request.started).isSuccess(wasDelayed -> assertThat(wasDelayed).isFalse()); + assertThat(throttler.getStoredPermits()).isEqualTo(0); + assertThat(throttler.getQueue()).isEmpty(); + } + + @Test + public void should_enqueue_when_over_rate() { + // Given + for (int i = 0; i < 5; i++) { + throttler.register(new MockThrottled()); + } + assertThat(throttler.getStoredPermits()).isEqualTo(0); + + // When + // (do not advance time) + MockThrottled request = new MockThrottled(); + throttler.register(request); + + // Then + assertThatStage(request.started).isNotDone(); + assertThat(throttler.getStoredPermits()).isEqualTo(0); + assertThat(throttler.getQueue()).containsExactly(request); + + ScheduledTaskCapturingEventLoop.CapturedTask task = adminExecutor.nextTask(); + assertThat(task).isNotNull(); + assertThat(task.getInitialDelay(TimeUnit.NANOSECONDS)).isEqualTo(DRAIN_INTERVAL.toNanos()); + } + + @Test + public void should_reject_when_queue_is_full() { + // Given + for (int i = 0; i < 15; i++) { + throttler.register(new MockThrottled()); + } + assertThat(throttler.getStoredPermits()).isEqualTo(0); + assertThat(throttler.getQueue()).hasSize(10); + + // When + clock.add(TWO_HUNDRED_MILLISECONDS); // even if time has passed, queued items have priority + MockThrottled request = new MockThrottled(); + throttler.register(request); + + // Then + assertThatStage(request.started) + .isFailed(error -> assertThat(error).isInstanceOf(RequestThrottlingException.class)); + } + + @Test + public void should_remove_timed_out_request_from_queue() { + // Given + for (int i = 0; i < 5; i++) { + throttler.register(new MockThrottled()); + } + MockThrottled queued1 = new MockThrottled(); + throttler.register(queued1); + MockThrottled queued2 = new MockThrottled(); + throttler.register(queued2); + + // When + throttler.signalTimeout(queued1); + + // Then + assertThatStage(queued2.started).isNotDone(); + assertThat(throttler.getStoredPermits()).isEqualTo(0); + assertThat(throttler.getQueue()).containsExactly(queued2); + } + + @Test + public void should_dequeue_when_draining_task_runs() { + // Given + for (int i = 0; i < 5; i++) { + throttler.register(new MockThrottled()); + } + + MockThrottled queued1 = new MockThrottled(); + throttler.register(queued1); + assertThatStage(queued1.started).isNotDone(); + MockThrottled queued2 = new MockThrottled(); + throttler.register(queued2); + assertThatStage(queued2.started).isNotDone(); + assertThat(throttler.getStoredPermits()).isEqualTo(0); + assertThat(throttler.getQueue()).hasSize(2); + + ScheduledTaskCapturingEventLoop.CapturedTask task = adminExecutor.nextTask(); + assertThat(task).isNotNull(); + assertThat(task.getInitialDelay(TimeUnit.NANOSECONDS)).isEqualTo(DRAIN_INTERVAL.toNanos()); + + // When + // (do not advance clock => no new permits) + task.run(); + + // Then + assertThat(throttler.getStoredPermits()).isEqualTo(0); + assertThat(throttler.getQueue()).containsExactly(queued1, queued2); + // task reschedules itself since it did not empty the queue + task = adminExecutor.nextTask(); + assertThat(task).isNotNull(); + assertThat(task.getInitialDelay(TimeUnit.NANOSECONDS)).isEqualTo(DRAIN_INTERVAL.toNanos()); + + // When + clock.add(TWO_HUNDRED_MILLISECONDS); // 1 extra permit issued + task.run(); + + // Then + assertThatStage(queued1.started).isSuccess(wasDelayed -> assertThat(wasDelayed).isTrue()); + assertThatStage(queued2.started).isNotDone(); + assertThat(throttler.getStoredPermits()).isEqualTo(0); + assertThat(throttler.getQueue()).containsExactly(queued2); + // task reschedules itself since it did not empty the queue + task = adminExecutor.nextTask(); + assertThat(task).isNotNull(); + assertThat(task.getInitialDelay(TimeUnit.NANOSECONDS)).isEqualTo(DRAIN_INTERVAL.toNanos()); + + // When + clock.add(TWO_HUNDRED_MILLISECONDS); + task.run(); + + // Then + assertThatStage(queued2.started).isSuccess(wasDelayed -> assertThat(wasDelayed).isTrue()); + assertThat(throttler.getStoredPermits()).isEqualTo(0); + assertThat(throttler.getQueue()).isEmpty(); + assertThat(adminExecutor.nextTask()).isNull(); + } + + @Test + public void should_store_new_permits_up_to_threshold() { + // Given + for (int i = 0; i < 5; i++) { + throttler.register(new MockThrottled()); + } + assertThat(throttler.getStoredPermits()).isEqualTo(0); + + // When + clock.add(TWO_SECONDS); // should store at most 1 second worth of permits + + // Then + // acquire to trigger the throttler to update its permits + throttler.register(new MockThrottled()); + assertThat(throttler.getStoredPermits()).isEqualTo(4); + } + + /** + * Ensure that permits are still created if we try to acquire faster than the minimal interval to + * create one permit. In an early version of the code there was a bug where we would reset the + * elapsed time on each acquisition attempt, and never regenerate permits. + */ + @Test + public void should_keep_accumulating_time_if_no_permits_created() { + // Given + for (int i = 0; i < 5; i++) { + throttler.register(new MockThrottled()); + } + assertThat(throttler.getStoredPermits()).isEqualTo(0); + + // When + clock.add(ONE_HUNDRED_MILLISECONDS); + + // Then + MockThrottled queued = new MockThrottled(); + throttler.register(queued); + assertThatStage(queued.started).isNotDone(); + + // When + clock.add(ONE_HUNDRED_MILLISECONDS); + adminExecutor.nextTask().run(); + + // Then + assertThatStage(queued.started).isSuccess(wasDelayed -> assertThat(wasDelayed).isTrue()); + } + + @Test + public void should_reject_enqueued_when_closing() { + // Given + for (int i = 0; i < 5; i++) { + throttler.register(new MockThrottled()); + } + List enqueued = Lists.newArrayList(); + for (int i = 0; i < 10; i++) { + MockThrottled request = new MockThrottled(); + throttler.register(request); + assertThatStage(request.started).isNotDone(); + enqueued.add(request); + } + + // When + throttler.close(); + + // Then + for (MockThrottled request : enqueued) { + assertThatStage(request.started) + .isFailed(error -> assertThat(error).isInstanceOf(RequestThrottlingException.class)); + } + + // When + MockThrottled request = new MockThrottled(); + throttler.register(request); + + // Then + assertThatStage(request.started) + .isFailed(error -> assertThat(error).isInstanceOf(RequestThrottlingException.class)); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/SettableNanoClock.java b/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/SettableNanoClock.java new file mode 100644 index 00000000000..b12fbf35582 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/session/throttling/SettableNanoClock.java @@ -0,0 +1,32 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.session.throttling; + +class SettableNanoClock implements NanoClock { + + private volatile long nanoTime; + + @Override + public long nanoTime() { + return nanoTime; + } + + // This is racy, but in our tests it's never read concurrently + @SuppressWarnings("NonAtomicVolatileUpdate") + void add(long increment) { + nanoTime += increment; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/time/AtomicTimestampGeneratorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/time/AtomicTimestampGeneratorTest.java new file mode 100644 index 00000000000..fa4adec9e6c --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/time/AtomicTimestampGeneratorTest.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.time; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.fail; +import static org.mockito.Mockito.when; + +import java.util.SortedSet; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.mockito.stubbing.OngoingStubbing; + +public class AtomicTimestampGeneratorTest extends MonotonicTimestampGeneratorTestBase { + @Override + protected MonotonicTimestampGenerator newInstance(Clock clock) { + return new AtomicTimestampGenerator(clock, context); + } + + @Test + public void should_share_timestamps_across_all_threads() throws Exception { + // Prepare to generate 1000 timestamps with the clock frozen at 1 + OngoingStubbing stub = when(clock.currentTimeMicros()); + for (int i = 0; i < 1000; i++) { + stub = stub.thenReturn(1L); + } + + MonotonicTimestampGenerator generator = newInstance(clock); + + final int testThreadsCount = 2; + assertThat(1000 % testThreadsCount).isZero(); + + final SortedSet allTimestamps = new ConcurrentSkipListSet(); + ExecutorService executor = Executors.newFixedThreadPool(testThreadsCount); + for (int i = 0; i < testThreadsCount; i++) { + executor.submit( + () -> { + for (int j = 0; j < 1000 / testThreadsCount; j++) { + allTimestamps.add(generator.next()); + } + }); + } + executor.shutdown(); + if (!executor.awaitTermination(1, TimeUnit.SECONDS)) { + fail("Expected executor to shut down cleanly"); + } + + assertThat(allTimestamps).hasSize(1000); + assertThat(allTimestamps.first()).isEqualTo(1); + assertThat(allTimestamps.last()).isEqualTo(1000); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/time/MonotonicTimestampGeneratorTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/time/MonotonicTimestampGeneratorTestBase.java new file mode 100644 index 00000000000..59324205872 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/time/MonotonicTimestampGeneratorTestBase.java @@ -0,0 +1,141 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.time; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfig; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import java.time.Duration; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.OngoingStubbing; +import org.slf4j.LoggerFactory; + +abstract class MonotonicTimestampGeneratorTestBase { + + @Mock protected Clock clock; + @Mock protected InternalDriverContext context; + @Mock private DriverConfig config; + @Mock protected DriverExecutionProfile defaultProfile; + + @Mock private Appender appender; + @Captor private ArgumentCaptor loggingEventCaptor; + + private Logger logger; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(config.getDefaultProfile()).thenReturn(defaultProfile); + when(context.getConfig()).thenReturn(config); + + // Disable warnings by default + when(defaultProfile.getDuration( + DefaultDriverOption.TIMESTAMP_GENERATOR_DRIFT_WARNING_THRESHOLD, Duration.ZERO)) + .thenReturn(Duration.ZERO); + // Actual value doesn't really matter since we only test the first warning + when(defaultProfile.getDuration(DefaultDriverOption.TIMESTAMP_GENERATOR_DRIFT_WARNING_INTERVAL)) + .thenReturn(Duration.ofSeconds(10)); + + logger = (Logger) LoggerFactory.getLogger(MonotonicTimestampGenerator.class); + logger.addAppender(appender); + } + + @After + public void teardown() { + logger.detachAppender(appender); + } + + protected abstract MonotonicTimestampGenerator newInstance(Clock clock); + + @Test + public void should_use_clock_if_it_keeps_increasing() { + OngoingStubbing stub = when(clock.currentTimeMicros()); + for (long l = 1; l < 5; l++) { + stub = stub.thenReturn(l); + } + + MonotonicTimestampGenerator generator = newInstance(clock); + + for (long l = 1; l < 5; l++) { + assertThat(generator.next()).isEqualTo(l); + } + } + + @Test + public void should_increment_if_clock_does_not_increase() { + when(clock.currentTimeMicros()).thenReturn(1L, 1L, 1L, 5L); + + MonotonicTimestampGenerator generator = newInstance(clock); + + assertThat(generator.next()).isEqualTo(1); + assertThat(generator.next()).isEqualTo(2); + assertThat(generator.next()).isEqualTo(3); + assertThat(generator.next()).isEqualTo(5); + } + + @Test + public void should_warn_if_timestamps_drift() { + when(defaultProfile.getDuration( + DefaultDriverOption.TIMESTAMP_GENERATOR_DRIFT_WARNING_THRESHOLD, Duration.ZERO)) + .thenReturn(Duration.ofNanos(2 * 1000)); + when(clock.currentTimeMicros()).thenReturn(1L, 1L, 1L, 1L, 1L); + + MonotonicTimestampGenerator generator = newInstance(clock); + + assertThat(generator.next()).isEqualTo(1); + assertThat(generator.next()).isEqualTo(2); + assertThat(generator.next()).isEqualTo(3); + assertThat(generator.next()).isEqualTo(4); + // Clock still at 1, last returned timestamp is 4 (> 1 + 2), should warn + assertThat(generator.next()).isEqualTo(5); + + verify(appender).doAppend(loggingEventCaptor.capture()); + ILoggingEvent log = loggingEventCaptor.getValue(); + assertThat(log.getLevel()).isEqualTo(Level.WARN); + assertThat(log.getMessage()).contains("Clock skew detected"); + } + + @Test + public void should_go_back_to_clock_if_new_tick_high_enough() { + when(clock.currentTimeMicros()).thenReturn(1L, 1L, 1L, 1L, 1L, 10L); + + MonotonicTimestampGenerator generator = newInstance(clock); + + for (long l = 1; l <= 5; l++) { + // Clock at 1, keep incrementing + assertThat(generator.next()).isEqualTo(l); + } + + // Last returned is 5, but clock has ticked to 10, should use that. + assertThat(generator.next()).isEqualTo(10); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/time/ThreadLocalTimestampGeneratorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/time/ThreadLocalTimestampGeneratorTest.java new file mode 100644 index 00000000000..6de3a2b5e41 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/time/ThreadLocalTimestampGeneratorTest.java @@ -0,0 +1,77 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.time; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static com.datastax.oss.driver.Assertions.assertThatStage; +import static com.datastax.oss.driver.Assertions.fail; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.mockito.stubbing.OngoingStubbing; + +public class ThreadLocalTimestampGeneratorTest extends MonotonicTimestampGeneratorTestBase { + @Override + protected MonotonicTimestampGenerator newInstance(Clock clock) { + return new ThreadLocalTimestampGenerator(clock, context); + } + + @Test + public void should_confine_timestamps_to_thread() throws Exception { + final int testThreadsCount = 2; + + // Prepare to generate 1000 timestamps for each thread, with the clock frozen at 1 + OngoingStubbing stub = when(clock.currentTimeMicros()); + for (int i = 0; i < testThreadsCount * 1000; i++) { + stub = stub.thenReturn(1L); + } + + MonotonicTimestampGenerator generator = newInstance(clock); + + List> futures = new CopyOnWriteArrayList<>(); + ExecutorService executor = Executors.newFixedThreadPool(testThreadsCount); + for (int i = 0; i < testThreadsCount; i++) { + executor.submit( + () -> { + try { + for (long l = 1; l <= 1000; l++) { + assertThat(generator.next()).isEqualTo(l); + } + futures.add(CompletableFuture.completedFuture(null)); + } catch (Throwable t) { + futures.add(CompletableFutures.failedFuture(t)); + } + }); + } + executor.shutdown(); + if (!executor.awaitTermination(1, TimeUnit.SECONDS)) { + fail("Expected executor to shut down cleanly"); + } + + assertThat(futures).hasSize(testThreadsCount); + for (CompletionStage future : futures) { + assertThatStage(future).isSuccess(); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/tracker/RequestLogFormatterTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/tracker/RequestLogFormatterTest.java new file mode 100644 index 00000000000..160e5d04dd9 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/tracker/RequestLogFormatterTest.java @@ -0,0 +1,289 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.tracker; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.cql.BatchStatement; +import com.datastax.oss.driver.api.core.cql.BoundStatement; +import com.datastax.oss.driver.api.core.cql.ColumnDefinition; +import com.datastax.oss.driver.api.core.cql.DefaultBatchType; +import com.datastax.oss.driver.api.core.cql.PreparedStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.internal.core.cql.DefaultColumnDefinition; +import com.datastax.oss.driver.internal.core.cql.DefaultColumnDefinitions; +import com.datastax.oss.driver.internal.core.cql.DefaultPreparedStatement; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.protocol.internal.response.result.ColumnSpec; +import com.datastax.oss.protocol.internal.response.result.RawType; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.util.Collections; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class RequestLogFormatterTest { + + @Mock private DriverContext context; + private final ProtocolVersion protocolVersion = DefaultProtocolVersion.V4; + + private RequestLogFormatter formatter; + + @Before + public void setup() { + when(context.getCodecRegistry()).thenReturn(CodecRegistry.DEFAULT); + when(context.getProtocolVersion()).thenReturn(protocolVersion); + + formatter = new RequestLogFormatter(context); + } + + @Test + public void should_format_simple_statement_without_values() { + SimpleStatement statement = + SimpleStatement.newInstance("SELECT release_version FROM system.local"); + + assertThat( + formatRequest( + statement, Integer.MAX_VALUE, false, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[0 values] SELECT release_version FROM system.local"); + + assertThat( + formatRequest(statement, Integer.MAX_VALUE, true, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[0 values] SELECT release_version FROM system.local"); + + assertThat(formatRequest(statement, 20, false, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[0 values] SELECT release_versi..."); + } + + @Test + public void should_format_simple_statement_with_positional_values() { + SimpleStatement statement = + SimpleStatement.builder("UPDATE foo SET v=? WHERE k=?") + .addPositionalValue(Bytes.fromHexString("0xdeadbeef")) + .addPositionalValue(0) + .build(); + + assertThat( + formatRequest( + statement, Integer.MAX_VALUE, false, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[2 values] UPDATE foo SET v=? WHERE k=?"); + + assertThat( + formatRequest(statement, Integer.MAX_VALUE, true, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[2 values] UPDATE foo SET v=? WHERE k=? [v0=0xdeadbeef, v1=0]"); + + assertThat(formatRequest(statement, Integer.MAX_VALUE, true, 1, Integer.MAX_VALUE)) + .isEqualTo( + "[2 values] UPDATE foo SET v=? WHERE k=? [v0=0xdeadbeef, ...]"); + + assertThat(formatRequest(statement, Integer.MAX_VALUE, true, Integer.MAX_VALUE, 4)) + .isEqualTo("[2 values] UPDATE foo SET v=? WHERE k=? [v0=0xde..., v1=0]"); + } + + @Test + public void should_format_simple_statement_with_named_values() { + SimpleStatement statement = + SimpleStatement.builder("UPDATE foo SET v=:v WHERE k=:k") + .addNamedValue("v", Bytes.fromHexString("0xdeadbeef")) + .addNamedValue("k", 0) + .build(); + + assertThat( + formatRequest( + statement, Integer.MAX_VALUE, false, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[2 values] UPDATE foo SET v=:v WHERE k=:k"); + + assertThat( + formatRequest(statement, Integer.MAX_VALUE, true, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[2 values] UPDATE foo SET v=:v WHERE k=:k [v=0xdeadbeef, k=0]"); + + assertThat(formatRequest(statement, Integer.MAX_VALUE, true, 1, Integer.MAX_VALUE)) + .isEqualTo( + "[2 values] UPDATE foo SET v=:v WHERE k=:k [v=0xdeadbeef, ...]"); + + assertThat(formatRequest(statement, Integer.MAX_VALUE, true, Integer.MAX_VALUE, 4)) + .isEqualTo("[2 values] UPDATE foo SET v=:v WHERE k=:k [v=0xde..., k=0]"); + } + + @Test + public void should_format_bound_statement() { + PreparedStatement preparedStatement = + mockPreparedStatement( + "UPDATE foo SET v=? WHERE k=?", + ImmutableMap.of("v", DataTypes.BLOB, "k", DataTypes.INT)); + BoundStatement statement = preparedStatement.bind(Bytes.fromHexString("0xdeadbeef"), 0); + + assertThat( + formatRequest( + statement, Integer.MAX_VALUE, false, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[2 values] UPDATE foo SET v=? WHERE k=?"); + + assertThat( + formatRequest(statement, Integer.MAX_VALUE, true, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[2 values] UPDATE foo SET v=? WHERE k=? [v=0xdeadbeef, k=0]"); + + assertThat(formatRequest(statement, Integer.MAX_VALUE, true, 1, Integer.MAX_VALUE)) + .isEqualTo( + "[2 values] UPDATE foo SET v=? WHERE k=? [v=0xdeadbeef, ...]"); + + assertThat(formatRequest(statement, Integer.MAX_VALUE, true, Integer.MAX_VALUE, 4)) + .isEqualTo("[2 values] UPDATE foo SET v=? WHERE k=? [v=0xde..., k=0]"); + } + + @Test + public void should_format_bound_statement_with_unset_values() { + PreparedStatement preparedStatement = + mockPreparedStatement( + "UPDATE foo SET v=? WHERE k=?", + ImmutableMap.of("v", DataTypes.BLOB, "k", DataTypes.INT)); + BoundStatement statement = preparedStatement.bind().setInt("k", 0); + assertThat( + formatRequest(statement, Integer.MAX_VALUE, true, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[2 values] UPDATE foo SET v=? WHERE k=? [v=, k=0]"); + } + + @Test + public void should_format_batch_statement() { + SimpleStatement statement1 = + SimpleStatement.builder("UPDATE foo SET v=? WHERE k=?") + .addNamedValue("v", Bytes.fromHexString("0xdeadbeef")) + .addNamedValue("k", 0) + .build(); + + PreparedStatement preparedStatement = + mockPreparedStatement( + "UPDATE foo SET v=? WHERE k=?", + ImmutableMap.of("v", DataTypes.BLOB, "k", DataTypes.INT)); + BoundStatement statement2 = preparedStatement.bind(Bytes.fromHexString("0xabcdef"), 1); + + BatchStatement batch = + BatchStatement.builder(DefaultBatchType.UNLOGGED) + .addStatements(statement1, statement2) + .build(); + + assertThat(formatRequest(batch, Integer.MAX_VALUE, false, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo( + "[2 statements, 4 values] " + + "BEGIN UNLOGGED BATCH " + + "UPDATE foo SET v=? WHERE k=?; " + + "UPDATE foo SET v=? WHERE k=?; " + + "APPLY BATCH"); + + assertThat(formatRequest(batch, 20, false, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo("[2 statements, 4 values] BEGIN UNLOGGED BATCH..."); + + assertThat(formatRequest(batch, Integer.MAX_VALUE, true, Integer.MAX_VALUE, Integer.MAX_VALUE)) + .isEqualTo( + "[2 statements, 4 values] " + + "BEGIN UNLOGGED BATCH " + + "UPDATE foo SET v=? WHERE k=?; " + + "UPDATE foo SET v=? WHERE k=?; " + + "APPLY BATCH " + + "[v=0xdeadbeef, k=0]" + + "[v=0xabcdef, k=1]"); + + assertThat(formatRequest(batch, Integer.MAX_VALUE, true, 3, Integer.MAX_VALUE)) + .isEqualTo( + "[2 statements, 4 values] " + + "BEGIN UNLOGGED BATCH " + + "UPDATE foo SET v=? WHERE k=?; " + + "UPDATE foo SET v=? WHERE k=?; " + + "APPLY BATCH " + + "[v=0xdeadbeef, k=0]" + + "[v=0xabcdef, ...]"); + + assertThat(formatRequest(batch, Integer.MAX_VALUE, true, 2, Integer.MAX_VALUE)) + .isEqualTo( + "[2 statements, 4 values] " + + "BEGIN UNLOGGED BATCH " + + "UPDATE foo SET v=? WHERE k=?; " + + "UPDATE foo SET v=? WHERE k=?; " + + "APPLY BATCH " + + "[v=0xdeadbeef, k=0]" + + "[...]"); + + assertThat(formatRequest(batch, Integer.MAX_VALUE, true, Integer.MAX_VALUE, 4)) + .isEqualTo( + "[2 statements, 4 values] " + + "BEGIN UNLOGGED BATCH " + + "UPDATE foo SET v=? WHERE k=?; " + + "UPDATE foo SET v=? WHERE k=?; " + + "APPLY BATCH " + + "[v=0xde..., k=0]" + + "[v=0xab..., k=1]"); + } + + private String formatRequest( + Request request, int maxQueryLength, boolean showValues, int maxValues, int maxValueLength) { + StringBuilder builder = new StringBuilder(); + formatter.appendRequest( + request, maxQueryLength, showValues, maxValues, maxValueLength, builder); + return builder.toString(); + } + + private PreparedStatement mockPreparedStatement(String query, Map variables) { + ImmutableList.Builder definitions = ImmutableList.builder(); + int i = 0; + for (Map.Entry entry : variables.entrySet()) { + definitions.add( + new DefaultColumnDefinition( + new ColumnSpec( + "test", + "foo", + entry.getKey(), + i, + RawType.PRIMITIVES.get(entry.getValue().getProtocolCode())), + context)); + } + return new DefaultPreparedStatement( + Bytes.fromHexString("0x"), + query, + DefaultColumnDefinitions.valueOf(definitions.build()), + Collections.emptyList(), + null, + null, + null, + Collections.emptyMap(), + null, + null, + null, + null, + null, + Collections.emptyMap(), + null, + null, + null, + Integer.MIN_VALUE, + null, + null, + false, + context.getCodecRegistry(), + context.getProtocolVersion()); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/DataTypeDetachableTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/DataTypeDetachableTest.java new file mode 100644 index 00000000000..e8dac19237b --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/DataTypeDetachableTest.java @@ -0,0 +1,186 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.MapType; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.SerializationHelper; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class DataTypeDetachableTest { + + @Mock private AttachmentPoint attachmentPoint; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void simple_types_should_never_be_detached() { + // Because simple types don't need the codec registry, we consider them as always attached by + // default + for (DataType simpleType : ImmutableList.of(DataTypes.INT, DataTypes.custom("some.class"))) { + assertThat(simpleType.isDetached()).isFalse(); + assertThat(SerializationHelper.serializeAndDeserialize(simpleType).isDetached()).isFalse(); + } + } + + @Test + public void manually_created_tuple_should_be_detached() { + TupleType tuple = DataTypes.tupleOf(DataTypes.INT, DataTypes.TEXT); + assertThat(tuple.isDetached()).isTrue(); + } + + @Test + public void attaching_tuple_should_attach_all_of_its_subtypes() { + TupleType tuple1 = DataTypes.tupleOf(DataTypes.INT); + TupleType tuple2 = DataTypes.tupleOf(DataTypes.TEXT, tuple1); + + assertThat(tuple1.isDetached()).isTrue(); + assertThat(tuple2.isDetached()).isTrue(); + + tuple2.attach(attachmentPoint); + + assertThat(tuple1.isDetached()).isFalse(); + } + + @Test + public void manually_created_udt_should_be_detached() { + UserDefinedType udt = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("field1"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("field2"), DataTypes.TEXT) + .build(); + assertThat(udt.isDetached()).isTrue(); + } + + @Test + public void attaching_udt_should_attach_all_of_its_subtypes() { + TupleType tuple = DataTypes.tupleOf(DataTypes.INT); + UserDefinedType udt = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("field1"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("field2"), tuple) + .build(); + + assertThat(tuple.isDetached()).isTrue(); + assertThat(udt.isDetached()).isTrue(); + + udt.attach(attachmentPoint); + + assertThat(tuple.isDetached()).isFalse(); + } + + @Test + public void list_should_be_attached_if_its_element_is() { + TupleType tuple = DataTypes.tupleOf(DataTypes.INT); + ListType list = DataTypes.listOf(tuple); + + assertThat(tuple.isDetached()).isTrue(); + assertThat(list.isDetached()).isTrue(); + + tuple.attach(attachmentPoint); + + assertThat(list.isDetached()).isFalse(); + } + + @Test + public void attaching_list_should_attach_its_element() { + TupleType tuple = DataTypes.tupleOf(DataTypes.INT); + ListType list = DataTypes.listOf(tuple); + + assertThat(tuple.isDetached()).isTrue(); + assertThat(list.isDetached()).isTrue(); + + list.attach(attachmentPoint); + + assertThat(tuple.isDetached()).isFalse(); + } + + @Test + public void set_should_be_attached_if_its_element_is() { + TupleType tuple = DataTypes.tupleOf(DataTypes.INT); + SetType set = DataTypes.setOf(tuple); + + assertThat(tuple.isDetached()).isTrue(); + assertThat(set.isDetached()).isTrue(); + + tuple.attach(attachmentPoint); + + assertThat(set.isDetached()).isFalse(); + } + + @Test + public void attaching_set_should_attach_its_element() { + TupleType tuple = DataTypes.tupleOf(DataTypes.INT); + SetType set = DataTypes.setOf(tuple); + + assertThat(tuple.isDetached()).isTrue(); + assertThat(set.isDetached()).isTrue(); + + set.attach(attachmentPoint); + + assertThat(tuple.isDetached()).isFalse(); + } + + @Test + public void map_should_be_attached_if_its_elements_are() { + TupleType tuple1 = DataTypes.tupleOf(DataTypes.INT); + TupleType tuple2 = DataTypes.tupleOf(DataTypes.TEXT); + MapType map = DataTypes.mapOf(tuple1, tuple2); + + assertThat(tuple1.isDetached()).isTrue(); + assertThat(tuple2.isDetached()).isTrue(); + assertThat(map.isDetached()).isTrue(); + + tuple1.attach(attachmentPoint); + assertThat(map.isDetached()).isTrue(); + + tuple2.attach(attachmentPoint); + assertThat(map.isDetached()).isFalse(); + } + + @Test + public void attaching_map_should_attach_all_of_its_subtypes() { + TupleType tuple1 = DataTypes.tupleOf(DataTypes.INT); + TupleType tuple2 = DataTypes.tupleOf(DataTypes.TEXT); + MapType map = DataTypes.mapOf(tuple1, tuple2); + + assertThat(tuple1.isDetached()).isTrue(); + assertThat(tuple2.isDetached()).isTrue(); + + map.attach(attachmentPoint); + + assertThat(tuple1.isDetached()).isFalse(); + assertThat(tuple2.isDetached()).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/DataTypeSerializationTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/DataTypeSerializationTest.java new file mode 100644 index 00000000000..ed53b0b4e65 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/DataTypeSerializationTest.java @@ -0,0 +1,61 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.internal.SerializationHelper; +import org.junit.Test; + +public class DataTypeSerializationTest { + + @Test + public void should_serialize_and_deserialize() { + TupleType tuple = DataTypes.tupleOf(DataTypes.INT, DataTypes.TEXT); + UserDefinedType udt = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("field1"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("field2"), DataTypes.TEXT) + .build(); + + // Because primitive and custom types never use the codec registry, we consider them always + // attached + should_serialize_and_deserialize(DataTypes.INT, false); + should_serialize_and_deserialize(DataTypes.custom("some.class.name"), false); + + should_serialize_and_deserialize(tuple, true); + should_serialize_and_deserialize(udt, true); + should_serialize_and_deserialize(DataTypes.listOf(DataTypes.INT), false); + should_serialize_and_deserialize(DataTypes.listOf(tuple), true); + should_serialize_and_deserialize(DataTypes.setOf(udt), true); + should_serialize_and_deserialize(DataTypes.mapOf(tuple, udt), true); + } + + private void should_serialize_and_deserialize(DataType in, boolean expectDetached) { + // When + DataType out = SerializationHelper.serializeAndDeserialize(in); + + // Then + assertThat(out).isEqualTo(in); + assertThat(out.isDetached()).isEqualTo(expectDetached); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/BigIntCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/BigIntCodecTest.java new file mode 100644 index 00000000000..a2d7fd91ee0 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/BigIntCodecTest.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import org.junit.Test; + +public class BigIntCodecTest extends CodecTestBase { + + public BigIntCodecTest() { + this.codec = TypeCodecs.BIGINT; + } + + @Test + public void should_encode() { + assertThat(encode(1L)).isEqualTo("0x0000000000000001"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x0000000000000001")).isEqualTo(1L); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x0000000000000000" + "0000"); + } + + @Test + public void should_format() { + assertThat(format(1L)).isEqualTo("1"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("1")).isEqualTo(1L); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a number"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_if_out_of_range() { + parse(Long.toString(Long.MAX_VALUE) + "0"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(Long.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(long.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(Long.class)).isTrue(); + assertThat(codec.accepts(long.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(123L)).isTrue(); + assertThat(codec.accepts(Long.MIN_VALUE)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/BlobCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/BlobCodecTest.java new file mode 100644 index 00000000000..c0448e38dbd --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/BlobCodecTest.java @@ -0,0 +1,106 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import org.junit.Test; + +public class BlobCodecTest extends CodecTestBase { + private static final ByteBuffer BUFFER = Bytes.fromHexString("0xcafebabe"); + + public BlobCodecTest() { + this.codec = TypeCodecs.BLOB; + } + + @Test + public void should_encode() { + assertThat(encode(BUFFER)).isEqualTo("0xcafebabe"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_not_share_position_between_input_and_encoded() { + int inputPosition = BUFFER.position(); + ByteBuffer encoded = codec.encode(BUFFER, ProtocolVersion.DEFAULT); + // Read from the encoded buffer to change its position + encoded.get(); + // The input buffer should not be affected + assertThat(BUFFER.position()).isEqualTo(inputPosition); + } + + @Test + public void should_decode() { + assertThat(decode("0xcafebabe")).isEqualTo(BUFFER); + assertThat(decode("0x").capacity()).isEqualTo(0); + assertThat(decode(null)).isNull(); + } + + @Test + public void should_not_share_position_between_decoded_and_input() { + int inputPosition = BUFFER.position(); + ByteBuffer decoded = codec.decode(BUFFER, ProtocolVersion.DEFAULT); + // Read from the decoded buffer to change its position + decoded.get(); + // The input buffer should not be affected + assertThat(BUFFER.position()).isEqualTo(inputPosition); + } + + @Test + public void should_format() { + assertThat(format(BUFFER)).isEqualTo("0xcafebabe"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("0xcafebabe")).isEqualTo(BUFFER); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a blob"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(ByteBuffer.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(MappedByteBuffer.class))) + .isFalse(); // covariance not allowed + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(ByteBuffer.class)).isTrue(); + assertThat(codec.accepts(MappedByteBuffer.class)).isFalse(); // covariance not allowed + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(BUFFER)).isTrue(); + assertThat(codec.accepts(MappedByteBuffer.allocate(0))).isTrue(); // covariance allowed + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/BooleanCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/BooleanCodecTest.java new file mode 100644 index 00000000000..9433984011f --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/BooleanCodecTest.java @@ -0,0 +1,92 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import org.junit.Test; + +public class BooleanCodecTest extends CodecTestBase { + + public BooleanCodecTest() { + this.codec = TypeCodecs.BOOLEAN; + } + + @Test + public void should_encode() { + assertThat(encode(false)).isEqualTo("0x00"); + assertThat(encode(true)).isEqualTo("0x01"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x00")).isFalse(); + assertThat(decode("0x01")).isTrue(); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x0000"); + } + + @Test + public void should_format() { + assertThat(format(true)).isEqualTo("true"); + assertThat(format(false)).isEqualTo("false"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("true")).isEqualTo(true); + assertThat(parse("false")).isEqualTo(false); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("maybe"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(Boolean.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(boolean.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(Boolean.class)).isTrue(); + assertThat(codec.accepts(boolean.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(true)).isTrue(); + assertThat(codec.accepts(Boolean.TRUE)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CodecTestBase.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CodecTestBase.java new file mode 100644 index 00000000000..5fba391f94f --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CodecTestBase.java @@ -0,0 +1,59 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; + +public class CodecTestBase { + protected TypeCodec codec; + + protected String encode(T t, ProtocolVersion protocolVersion) { + assertThat(codec).as("Must set codec before calling this method").isNotNull(); + ByteBuffer bytes = codec.encode(t, protocolVersion); + return (bytes == null) ? null : Bytes.toHexString(bytes); + } + + protected String encode(T t) { + return encode(t, ProtocolVersion.DEFAULT); + } + + protected T decode(String hexString, ProtocolVersion protocolVersion) { + assertThat(codec).as("Must set codec before calling this method").isNotNull(); + ByteBuffer bytes = (hexString == null) ? null : Bytes.fromHexString(hexString); + // Decode twice, to assert that decode leaves the input buffer in its original state + codec.decode(bytes, protocolVersion); + return codec.decode(bytes, protocolVersion); + } + + protected T decode(String hexString) { + return decode(hexString, ProtocolVersion.DEFAULT); + } + + protected String format(T t) { + assertThat(codec).as("Must set codec before calling this method").isNotNull(); + return codec.format(t); + } + + protected T parse(String s) { + assertThat(codec).as("Must set codec before calling this method").isNotNull(); + return codec.parse(s); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CounterCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CounterCodecTest.java new file mode 100644 index 00000000000..70dbd91c305 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CounterCodecTest.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import org.junit.Test; + +public class CounterCodecTest extends CodecTestBase { + + public CounterCodecTest() { + this.codec = TypeCodecs.COUNTER; + } + + @Test + public void should_encode() { + assertThat(encode(1L)).isEqualTo("0x0000000000000001"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x0000000000000001")).isEqualTo(1L); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x0000000000000000" + "0000"); + } + + @Test + public void should_format() { + assertThat(format(1L)).isEqualTo("1"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("1")).isEqualTo(1L); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a number"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_if_out_of_range() { + parse(Long.toString(Long.MAX_VALUE) + "0"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(Long.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(long.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(Long.class)).isTrue(); + assertThat(codec.accepts(long.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(123L)).isTrue(); + assertThat(codec.accepts(Long.MIN_VALUE)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CqlDurationCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CqlDurationCodecTest.java new file mode 100644 index 00000000000..3a8e36d8c42 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CqlDurationCodecTest.java @@ -0,0 +1,94 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.data.CqlDuration; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import org.junit.Test; + +public class CqlDurationCodecTest extends CodecTestBase { + + private static final CqlDuration DURATION = CqlDuration.newInstance(1, 2, 3); + + public CqlDurationCodecTest() { + this.codec = TypeCodecs.DURATION; + } + + @Test + public void should_encode() { + assertThat(encode(DURATION)) + .isEqualTo( + "0x" + + "02" // 1 (encoded as 2 because of zig-zag encoding) + + "04" // 2 (same) + + "06" // 3 (same) + ); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x020406")).isEqualTo(DURATION); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalStateException.class) + public void should_fail_to_decode_if_not_enough_bytes() { + decode("0x0000"); + } + + @Test + public void should_format() { + assertThat(format(DURATION)).isEqualTo("1mo2d3ns"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("1mo2d3ns")).isEqualTo(DURATION); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a duration"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(CqlDuration.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(CqlDuration.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(DURATION)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CqlIntToStringCodec.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CqlIntToStringCodec.java new file mode 100644 index 00000000000..f52d139f1b4 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CqlIntToStringCodec.java @@ -0,0 +1,70 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; + +/** + * A sample user codec implementation that we use in our tests. + * + *

It maps a CQL string to a Java string containing its textual representation. + */ +public class CqlIntToStringCodec implements TypeCodec { + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.STRING; + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.INT; + } + + @Override + public ByteBuffer encode(String value, @NonNull ProtocolVersion protocolVersion) { + if (value == null) { + return null; + } else { + return TypeCodecs.INT.encode(Integer.parseInt(value), protocolVersion); + } + } + + @Override + public String decode(ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + return TypeCodecs.INT.decode(bytes, protocolVersion).toString(); + } + + @NonNull + @Override + public String format(String value) { + throw new UnsupportedOperationException("Not implemented for this test"); + } + + @Override + public String parse(String value) { + throw new UnsupportedOperationException("Not implemented for this test"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CustomCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CustomCodecTest.java new file mode 100644 index 00000000000..545b03c1f4f --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/CustomCodecTest.java @@ -0,0 +1,107 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import org.junit.Test; + +public class CustomCodecTest extends CodecTestBase { + private static final ByteBuffer BUFFER = Bytes.fromHexString("0xcafebabe"); + + public CustomCodecTest() { + this.codec = TypeCodecs.custom(DataTypes.custom("com.test.MyClass")); + } + + @Test + public void should_encode() { + assertThat(encode(BUFFER)).isEqualTo("0xcafebabe"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_not_share_position_between_input_and_encoded() { + int inputPosition = BUFFER.position(); + ByteBuffer encoded = codec.encode(BUFFER, ProtocolVersion.DEFAULT); + // Read from the encoded buffer to change its position + encoded.get(); + // The input buffer should not be affected + assertThat(BUFFER.position()).isEqualTo(inputPosition); + } + + @Test + public void should_decode() { + assertThat(decode("0xcafebabe")).isEqualTo(BUFFER); + assertThat(decode("0x").capacity()).isEqualTo(0); + assertThat(decode(null)).isNull(); + } + + @Test + public void should_not_share_position_between_decoded_and_input() { + int inputPosition = BUFFER.position(); + ByteBuffer decoded = codec.decode(BUFFER, ProtocolVersion.DEFAULT); + // Read from the decoded buffer to change its position + decoded.get(); + // The input buffer should not be affected + assertThat(BUFFER.position()).isEqualTo(inputPosition); + } + + @Test + public void should_format() { + assertThat(format(BUFFER)).isEqualTo("0xcafebabe"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("0xcafebabe")).isEqualTo(BUFFER); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a blob"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(ByteBuffer.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(MappedByteBuffer.class))) + .isFalse(); // covariance not allowed + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(ByteBuffer.class)).isTrue(); + assertThat(codec.accepts(MappedByteBuffer.class)).isFalse(); // covariance not allowed + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(BUFFER)).isTrue(); + assertThat(codec.accepts(MappedByteBuffer.allocate(0))).isTrue(); // covariance allowed + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/DateCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/DateCodecTest.java new file mode 100644 index 00000000000..55fcf4c78a9 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/DateCodecTest.java @@ -0,0 +1,108 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import java.time.LocalDate; +import org.junit.Test; + +public class DateCodecTest extends CodecTestBase { + + private static final LocalDate EPOCH = LocalDate.ofEpochDay(0); + private static final LocalDate MIN = LocalDate.parse("-5877641-06-23"); + private static final LocalDate MAX = LocalDate.parse("+5881580-07-11"); + + public DateCodecTest() { + this.codec = TypeCodecs.DATE; + } + + @Test + public void should_encode() { + // Dates are encoded as a number of days since the epoch, stored on 8 bytes with 0 in the + // middle. + assertThat(encode(MIN)).isEqualTo("0x00000000"); + // The "middle" is the one that has only the most significant bit set (because it has the same + // number of values before and after it, determined by all possible combinations of the + // remaining bits) + assertThat(encode(EPOCH)).isEqualTo("0x80000000"); + assertThat(encode(MAX)).isEqualTo("0xffffffff"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x00000000")).isEqualTo(MIN); + assertThat(decode("0x80000000")).isEqualTo(EPOCH); + assertThat(decode("0xffffffff")).isEqualTo(MAX); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x00000000" + "0000"); + } + + @Test + public void should_format() { + // No need to test various values because the codec delegates directly to the JDK's formatter, + // which we assume does its job correctly. + assertThat(format(EPOCH)).isEqualTo("'1970-01-01'"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + // Raw number + assertThat(parse("0")).isEqualTo(MIN); + assertThat(parse("2147483648")).isEqualTo(EPOCH); + + // Date format + assertThat(parse("'-5877641-06-23'")).isEqualTo(MIN); + assertThat(parse("'1970-01-01'")).isEqualTo(EPOCH); + assertThat(parse("'2014-01-01'")).isEqualTo(LocalDate.parse("2014-01-01")); + + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a date"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(LocalDate.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(LocalDate.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(EPOCH)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/DecimalCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/DecimalCodecTest.java new file mode 100644 index 00000000000..b0c7e2bec79 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/DecimalCodecTest.java @@ -0,0 +1,100 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import java.math.BigDecimal; +import org.junit.Test; + +public class DecimalCodecTest extends CodecTestBase { + + public DecimalCodecTest() { + this.codec = TypeCodecs.DECIMAL; + } + + @Test + public void should_encode() { + assertThat(encode(BigDecimal.ONE)) + .isEqualTo( + "0x" + + "00000000" // scale + + "01" // unscaled value + ); + assertThat(encode(BigDecimal.valueOf(128, 4))) + .isEqualTo( + "0x" + + "00000004" // scale + + "0080" // unscaled value + ); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x0000000001")).isEqualTo(BigDecimal.ONE); + assertThat(decode("0x000000040080")).isEqualTo(BigDecimal.valueOf(128, 4)); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_not_enough_bytes() { + decode("0x0000"); + } + + @Test + public void should_format() { + assertThat(format(BigDecimal.ONE)).isEqualTo("1"); + assertThat(format(BigDecimal.valueOf(128, 4))).isEqualTo("0.0128"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("1")).isEqualTo(BigDecimal.ONE); + assertThat(parse("0.0128")).isEqualTo(BigDecimal.valueOf(128, 4)); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a decimal"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(BigDecimal.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(BigDecimal.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(BigDecimal.ONE)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/DoubleCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/DoubleCodecTest.java new file mode 100644 index 00000000000..2c72249b597 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/DoubleCodecTest.java @@ -0,0 +1,90 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import org.junit.Test; + +public class DoubleCodecTest extends CodecTestBase { + + public DoubleCodecTest() { + this.codec = TypeCodecs.DOUBLE; + } + + @Test + public void should_encode() { + // Our codec relies on the JDK's ByteBuffer API. We're not testing the JDK, so no need to try + // a thousand different values. + assertThat(encode(0.0)).isEqualTo("0x0000000000000000"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x0000000000000000")).isEqualTo(0.0); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x0000"); + } + + @Test + public void should_format() { + assertThat(format(0.0)).isEqualTo("0.0"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("0.0")).isEqualTo(0.0); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a double"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(Double.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(double.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(Double.class)).isTrue(); + assertThat(codec.accepts(double.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(123.45d)).isTrue(); + assertThat(codec.accepts(Double.MIN_VALUE)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/FloatCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/FloatCodecTest.java new file mode 100644 index 00000000000..6864e3a788a --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/FloatCodecTest.java @@ -0,0 +1,90 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import org.junit.Test; + +public class FloatCodecTest extends CodecTestBase { + + public FloatCodecTest() { + this.codec = TypeCodecs.FLOAT; + } + + @Test + public void should_encode() { + // Our codec relies on the JDK's ByteBuffer API. We're not testing the JDK, so no need to try + // a thousand different values. + assertThat(encode(0.0f)).isEqualTo("0x00000000"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x00000000")).isEqualTo(0.0f); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x0000"); + } + + @Test + public void should_format() { + assertThat(format(0.0f)).isEqualTo("0.0"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("0.0")).isEqualTo(0.0f); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a float"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(Float.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(float.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(Float.class)).isTrue(); + assertThat(codec.accepts(float.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(123.45f)).isTrue(); + assertThat(codec.accepts(Float.MIN_VALUE)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/InetCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/InetCodecTest.java new file mode 100644 index 00000000000..e47c74ba8c1 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/InetCodecTest.java @@ -0,0 +1,118 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.base.Strings; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import org.junit.Test; + +public class InetCodecTest extends CodecTestBase { + + private static final InetAddress V4_ADDRESS; + private static final InetAddress V6_ADDRESS; + + static { + try { + V4_ADDRESS = InetAddress.getByName("127.0.0.1"); + V6_ADDRESS = InetAddress.getByName("::1"); + } catch (UnknownHostException e) { + fail("unexpected error", e); + throw new AssertionError(); // never reached + } + } + + public InetCodecTest() { + this.codec = TypeCodecs.INET; + } + + @Test + public void should_encode() { + assertThat(encode(V4_ADDRESS)).isEqualTo("0x7f000001"); + assertThat(encode(V6_ADDRESS)).isEqualTo("0x00000000000000000000000000000001"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x7f000001")).isEqualTo(V4_ADDRESS); + assertThat(decode("0x00000000000000000000000000000001")).isEqualTo(V6_ADDRESS); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_not_enough_bytes() { + decode("0x0000"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_incorrect_byte_count() { + decode("0x" + Strings.repeat("00", 7)); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x" + Strings.repeat("00", 17)); + } + + @Test + public void should_format() { + assertThat(format(V4_ADDRESS)).isEqualTo("'127.0.0.1'"); + assertThat(format(V6_ADDRESS)).isEqualTo("'0:0:0:0:0:0:0:1'"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("'127.0.0.1'")).isEqualTo(V4_ADDRESS); + assertThat(parse("'0:0:0:0:0:0:0:1'")).isEqualTo(V6_ADDRESS); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not an address"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(InetAddress.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Inet4Address.class))) + .isFalse(); // covariance not allowed + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(InetAddress.class)).isTrue(); + assertThat(codec.accepts(Inet4Address.class)).isFalse(); // covariance not allowed + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(V4_ADDRESS)).isTrue(); // covariance allowed + assertThat(codec.accepts(V6_ADDRESS)).isTrue(); // covariance allowed + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/IntCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/IntCodecTest.java new file mode 100644 index 00000000000..931934e3f55 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/IntCodecTest.java @@ -0,0 +1,95 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import org.junit.Test; + +public class IntCodecTest extends CodecTestBase { + + public IntCodecTest() { + this.codec = TypeCodecs.INT; + } + + @Test + public void should_encode() { + // Our codec relies on the JDK's ByteBuffer API. We're not testing the JDK, so no need to try + // a thousand different values. + assertThat(encode(0)).isEqualTo("0x00000000"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x00000000")).isEqualTo(0); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_not_enough_bytes() { + decode("0x0000"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x0000000000000000"); + } + + @Test + public void should_format() { + assertThat(format(0)).isEqualTo("0"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("0")).isEqualTo(0); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not an int"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(Integer.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(int.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Long.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(Integer.class)).isTrue(); + assertThat(codec.accepts(int.class)).isTrue(); + assertThat(codec.accepts(Long.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(123)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isTrue(); + assertThat(codec.accepts(Long.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/ListCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/ListCodecTest.java new file mode 100644 index 00000000000..7260a2ee3ac --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/ListCodecTest.java @@ -0,0 +1,139 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ListCodecTest extends CodecTestBase> { + + @Mock private TypeCodec elementCodec; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(elementCodec.getCqlType()).thenReturn(DataTypes.INT); + when(elementCodec.getJavaType()).thenReturn(GenericType.INTEGER); + codec = TypeCodecs.listOf(elementCodec); + } + + @Test + public void should_encode_null() { + assertThat(encode(null)).isNull(); + } + + @Test + public void should_encode_empty_list() { + assertThat(encode(new ArrayList<>())).isEqualTo("0x00000000"); + } + + @Test + public void should_encode_non_empty_list() { + when(elementCodec.encode(1, ProtocolVersion.DEFAULT)).thenReturn(Bytes.fromHexString("0x01")); + when(elementCodec.encode(2, ProtocolVersion.DEFAULT)).thenReturn(Bytes.fromHexString("0x0002")); + when(elementCodec.encode(3, ProtocolVersion.DEFAULT)) + .thenReturn(Bytes.fromHexString("0x000003")); + + assertThat(encode(ImmutableList.of(1, 2, 3))) + .isEqualTo( + "0x" + + "00000003" // number of elements + + "0000000101" // size + contents of element 1 + + "000000020002" // size + contents of element 2 + + "00000003000003" // size + contents of element 3 + ); + } + + @Test + public void should_decode_null_as_empty_list() { + assertThat(decode(null)).isEmpty(); + } + + @Test + public void should_decode_empty_list() { + assertThat(decode("0x00000000")).isEmpty(); + } + + @Test + public void should_decode_non_empty_list() { + when(elementCodec.decode(Bytes.fromHexString("0x01"), ProtocolVersion.DEFAULT)).thenReturn(1); + when(elementCodec.decode(Bytes.fromHexString("0x0002"), ProtocolVersion.DEFAULT)).thenReturn(2); + when(elementCodec.decode(Bytes.fromHexString("0x000003"), ProtocolVersion.DEFAULT)) + .thenReturn(3); + + assertThat(decode("0x" + "00000003" + "0000000101" + "000000020002" + "00000003000003")) + .containsExactly(1, 2, 3); + } + + @Test + public void should_format_null_list() { + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_format_empty_list() { + assertThat(format(new ArrayList<>())).isEqualTo("[]"); + } + + @Test + public void should_format_non_empty_list() { + when(elementCodec.format(1)).thenReturn("a"); + when(elementCodec.format(2)).thenReturn("b"); + when(elementCodec.format(3)).thenReturn("c"); + + assertThat(format(ImmutableList.of(1, 2, 3))).isEqualTo("[a,b,c]"); + } + + @Test + public void should_parse_null_or_empty_string() { + assertThat(parse(null)).isNull(); + assertThat(parse("")).isNull(); + } + + @Test + public void should_parse_empty_list() { + assertThat(parse("[]")).isEmpty(); + } + + @Test + public void should_parse_non_empty_list() { + when(elementCodec.parse("a")).thenReturn(1); + when(elementCodec.parse("b")).thenReturn(2); + when(elementCodec.parse("c")).thenReturn(3); + + assertThat(parse("[a,b,c]")).containsExactly(1, 2, 3); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_malformed_list() { + parse("not a list"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/MapCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/MapCodecTest.java new file mode 100644 index 00000000000..96de17f75e8 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/MapCodecTest.java @@ -0,0 +1,177 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class MapCodecTest extends CodecTestBase> { + + @Mock private TypeCodec keyCodec; + @Mock private TypeCodec valueCodec; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(keyCodec.getCqlType()).thenReturn(DataTypes.TEXT); + when(keyCodec.getJavaType()).thenReturn(GenericType.STRING); + + when(valueCodec.getCqlType()).thenReturn(DataTypes.INT); + when(valueCodec.getJavaType()).thenReturn(GenericType.INTEGER); + codec = TypeCodecs.mapOf(keyCodec, valueCodec); + } + + @Test + public void should_encode_null() { + assertThat(encode(null)).isNull(); + } + + @Test + public void should_encode_empty_map() { + assertThat(encode(new LinkedHashMap<>())).isEqualTo("0x00000000"); + } + + @Test + public void should_encode_non_empty_map() { + when(keyCodec.encode("a", ProtocolVersion.DEFAULT)).thenReturn(Bytes.fromHexString("0x10")); + when(keyCodec.encode("b", ProtocolVersion.DEFAULT)).thenReturn(Bytes.fromHexString("0x2000")); + when(keyCodec.encode("c", ProtocolVersion.DEFAULT)).thenReturn(Bytes.fromHexString("0x300000")); + + when(valueCodec.encode(1, ProtocolVersion.DEFAULT)).thenReturn(Bytes.fromHexString("0x01")); + when(valueCodec.encode(2, ProtocolVersion.DEFAULT)).thenReturn(Bytes.fromHexString("0x0002")); + when(valueCodec.encode(3, ProtocolVersion.DEFAULT)).thenReturn(Bytes.fromHexString("0x000003")); + + assertThat(encode(ImmutableMap.of("a", 1, "b", 2, "c", 3))) + .isEqualTo( + "0x" + + "00000003" // number of key-value pairs + + "0000000110" // size + contents of key 1 + + "0000000101" // size + contents of value 1 + + "000000022000" // size + contents of key 2 + + "000000020002" // size + contents of value 2 + + "00000003300000" // size + contents of key 3 + + "00000003000003" // size + contents of value 3 + ); + } + + @Test + public void should_decode_null_as_empty_map() { + assertThat(decode(null)).isEmpty(); + } + + @Test + public void should_decode_empty_map() { + assertThat(decode("0x00000000")).isEmpty(); + } + + @Test + public void should_decode_non_empty_map() { + when(keyCodec.decode(Bytes.fromHexString("0x10"), ProtocolVersion.DEFAULT)).thenReturn("a"); + when(keyCodec.decode(Bytes.fromHexString("0x2000"), ProtocolVersion.DEFAULT)).thenReturn("b"); + when(keyCodec.decode(Bytes.fromHexString("0x300000"), ProtocolVersion.DEFAULT)).thenReturn("c"); + + when(valueCodec.decode(Bytes.fromHexString("0x01"), ProtocolVersion.DEFAULT)).thenReturn(1); + when(valueCodec.decode(Bytes.fromHexString("0x0002"), ProtocolVersion.DEFAULT)).thenReturn(2); + when(valueCodec.decode(Bytes.fromHexString("0x000003"), ProtocolVersion.DEFAULT)).thenReturn(3); + + assertThat( + decode( + "0x" + + "00000003" + + "0000000110" + + "0000000101" + + "000000022000" + + "000000020002" + + "00000003300000" + + "00000003000003")) + .containsOnlyKeys("a", "b", "c") + .containsEntry("a", 1) + .containsEntry("b", 2) + .containsEntry("c", 3); + } + + @Test + public void should_format_null_map() { + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_format_empty_map() { + assertThat(format(new LinkedHashMap<>())).isEqualTo("{}"); + } + + @Test + public void should_format_non_empty_map() { + when(keyCodec.format("a")).thenReturn("foo"); + when(keyCodec.format("b")).thenReturn("bar"); + when(keyCodec.format("c")).thenReturn("baz"); + + when(valueCodec.format(1)).thenReturn("qux"); + when(valueCodec.format(2)).thenReturn("quux"); + when(valueCodec.format(3)).thenReturn("quuz"); + + assertThat(format(ImmutableMap.of("a", 1, "b", 2, "c", 3))) + .isEqualTo("{foo:qux,bar:quux,baz:quuz}"); + } + + @Test + public void should_parse_null_or_empty_string() { + assertThat(parse(null)).isNull(); + assertThat(parse("")).isNull(); + } + + @Test + public void should_parse_empty_map() { + assertThat(parse("{}")).isEmpty(); + } + + @Test + public void should_parse_non_empty_map() { + when(keyCodec.parse("foo")).thenReturn("a"); + when(keyCodec.parse("bar")).thenReturn("b"); + when(keyCodec.parse("baz")).thenReturn("c"); + + when(valueCodec.parse("qux")).thenReturn(1); + when(valueCodec.parse("quux")).thenReturn(2); + when(valueCodec.parse("quuz")).thenReturn(3); + + assertThat(parse("{foo:qux,bar:quux,baz:quuz}")) + .containsOnlyKeys("a", "b", "c") + .containsEntry("a", 1) + .containsEntry("b", 2) + .containsEntry("c", 3); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_malformed_map() { + parse("not a map"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/SetCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/SetCodecTest.java new file mode 100644 index 00000000000..9e6b590d2f4 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/SetCodecTest.java @@ -0,0 +1,139 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.protocol.internal.util.Bytes; +import java.util.LinkedHashSet; +import java.util.Set; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class SetCodecTest extends CodecTestBase> { + + @Mock private TypeCodec elementCodec; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(elementCodec.getCqlType()).thenReturn(DataTypes.INT); + when(elementCodec.getJavaType()).thenReturn(GenericType.INTEGER); + codec = TypeCodecs.setOf(elementCodec); + } + + @Test + public void should_encode_null() { + assertThat(encode(null)).isNull(); + } + + @Test + public void should_encode_empty_set() { + assertThat(encode(new LinkedHashSet<>())).isEqualTo("0x00000000"); + } + + @Test + public void should_encode_non_empty_set() { + when(elementCodec.encode(1, ProtocolVersion.DEFAULT)).thenReturn(Bytes.fromHexString("0x01")); + when(elementCodec.encode(2, ProtocolVersion.DEFAULT)).thenReturn(Bytes.fromHexString("0x0002")); + when(elementCodec.encode(3, ProtocolVersion.DEFAULT)) + .thenReturn(Bytes.fromHexString("0x000003")); + + assertThat(encode(ImmutableSet.of(1, 2, 3))) + .isEqualTo( + "0x" + + "00000003" // number of elements + + "0000000101" // size + contents of element 1 + + "000000020002" // size + contents of element 2 + + "00000003000003" // size + contents of element 3 + ); + } + + @Test + public void should_decode_null_as_empty_set() { + assertThat(decode(null)).isEmpty(); + } + + @Test + public void should_decode_empty_set() { + assertThat(decode("0x00000000")).isEmpty(); + } + + @Test + public void should_decode_non_empty_set() { + when(elementCodec.decode(Bytes.fromHexString("0x01"), ProtocolVersion.DEFAULT)).thenReturn(1); + when(elementCodec.decode(Bytes.fromHexString("0x0002"), ProtocolVersion.DEFAULT)).thenReturn(2); + when(elementCodec.decode(Bytes.fromHexString("0x000003"), ProtocolVersion.DEFAULT)) + .thenReturn(3); + + assertThat(decode("0x" + "00000003" + "0000000101" + "000000020002" + "00000003000003")) + .containsExactly(1, 2, 3); + } + + @Test + public void should_format_null_set() { + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_format_empty_set() { + assertThat(format(new LinkedHashSet<>())).isEqualTo("{}"); + } + + @Test + public void should_format_non_empty_set() { + when(elementCodec.format(1)).thenReturn("a"); + when(elementCodec.format(2)).thenReturn("b"); + when(elementCodec.format(3)).thenReturn("c"); + + assertThat(format(ImmutableSet.of(1, 2, 3))).isEqualTo("{a,b,c}"); + } + + @Test + public void should_parse_null_or_empty_string() { + assertThat(parse(null)).isNull(); + assertThat(parse("")).isNull(); + } + + @Test + public void should_parse_empty_set() { + assertThat(parse("{}")).isEmpty(); + } + + @Test + public void should_parse_non_empty_set() { + when(elementCodec.parse("a")).thenReturn(1); + when(elementCodec.parse("b")).thenReturn(2); + when(elementCodec.parse("c")).thenReturn(3); + + assertThat(parse("{a,b,c}")).containsExactly(1, 2, 3); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_malformed_set() { + parse("not a set"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/SmallIntCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/SmallIntCodecTest.java new file mode 100644 index 00000000000..75c436e0475 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/SmallIntCodecTest.java @@ -0,0 +1,95 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import org.junit.Test; + +public class SmallIntCodecTest extends CodecTestBase { + + public SmallIntCodecTest() { + this.codec = TypeCodecs.SMALLINT; + } + + @Test + public void should_encode() { + // Our codec relies on the JDK's ByteBuffer API. We're not testing the JDK, so no need to try + // a thousand different values. + assertThat(encode((short) 0)).isEqualTo("0x0000"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x0000")).isEqualTo((short) 0); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_not_enough_bytes() { + decode("0x00"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x000000"); + } + + @Test + public void should_format() { + assertThat(format((short) 0)).isEqualTo("0"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("0")).isEqualTo((short) 0); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a smallint"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(Short.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(short.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(Short.class)).isTrue(); + assertThat(codec.accepts(short.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(((short) 123))).isTrue(); + assertThat(codec.accepts(Short.MIN_VALUE)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/StringCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/StringCodecTest.java new file mode 100644 index 00000000000..77f33c1ae93 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/StringCodecTest.java @@ -0,0 +1,81 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import org.junit.Test; + +public class StringCodecTest extends CodecTestBase { + + public StringCodecTest() { + // We don't test ASCII, since it only differs by the encoding used + this.codec = TypeCodecs.TEXT; + } + + @Test + public void should_encode() { + assertThat(encode("hello")).isEqualTo("0x68656c6c6f"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x68656c6c6f")).isEqualTo("hello"); + assertThat(decode("0x")).isEmpty(); + assertThat(decode(null)).isNull(); + } + + @Test + public void should_format() { + assertThat(format("hello")).isEqualTo("'hello'"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("'hello'")).isEqualTo("hello"); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a string"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(String.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(String.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts("hello")).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TimeCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TimeCodecTest.java new file mode 100644 index 00000000000..6c346b145aa --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TimeCodecTest.java @@ -0,0 +1,100 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; +import org.junit.Test; + +public class TimeCodecTest extends CodecTestBase { + + public TimeCodecTest() { + this.codec = TypeCodecs.TIME; + } + + @Test + public void should_encode() { + assertThat(encode(LocalTime.MIDNIGHT)).isEqualTo("0x0000000000000000"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x0000000000000000")).isEqualTo(LocalTime.MIDNIGHT); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_not_enough_bytes() { + decode("0x0000"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x0000000000000000" + "0000"); + } + + @Test + public void should_format() { + // No need to test various values because the codec delegates directly to the JDK's formatter, + // which we assume does its job correctly. + assertThat(format(LocalTime.MIDNIGHT)).isEqualTo("'00:00:00.000000000'"); + assertThat(format(LocalTime.NOON.plus(13799999994L, ChronoUnit.NANOS))) + .isEqualTo("'12:00:13.799999994'"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + // Raw number + assertThat(parse("'0'")).isEqualTo(LocalTime.MIDNIGHT); + + // String format + assertThat(parse("'00:00'")).isEqualTo(LocalTime.MIDNIGHT); + + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a time"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(LocalTime.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(LocalTime.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(LocalTime.MIDNIGHT)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TimeUuidCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TimeUuidCodecTest.java new file mode 100644 index 00000000000..ae89ce5d9a8 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TimeUuidCodecTest.java @@ -0,0 +1,75 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import java.util.UUID; +import org.junit.Test; + +public class TimeUuidCodecTest extends CodecTestBase { + + private static final UUID TIME_BASED = new UUID(6342305776366260711L, -5736720392086604862L); + private static final UUID NOT_TIME_BASED = new UUID(2, 1); + + public TimeUuidCodecTest() { + this.codec = TypeCodecs.TIMEUUID; + + assertThat(TIME_BASED.version()).isEqualTo(1); + assertThat(NOT_TIME_BASED.version()).isNotEqualTo(1); + } + + @Test + public void should_encode_time_uuid() { + assertThat(encode(TIME_BASED)).isEqualTo("0x58046580293811e7b0631332a5f033c2"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_not_encode_non_time_uuid() { + assertThat(codec.accepts(NOT_TIME_BASED)).isFalse(); + encode(NOT_TIME_BASED); + } + + @Test + public void should_format_time_uuid() { + assertThat(format(TIME_BASED)).isEqualTo("58046580-2938-11e7-b063-1332a5f033c2"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_not_format_non_time_uuid() { + format(NOT_TIME_BASED); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(UUID.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(UUID.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(TIME_BASED)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TimestampCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TimestampCodecTest.java new file mode 100644 index 00000000000..97a57c2fabc --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TimestampCodecTest.java @@ -0,0 +1,193 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static java.time.ZoneOffset.ofHours; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(DataProviderRunner.class) +public class TimestampCodecTest extends CodecTestBase { + + public TimestampCodecTest() { + // force a given timezone for reproducible results in should_format + codec = new TimestampCodec(ZoneOffset.UTC); + } + + @Test + public void should_encode() { + assertThat(encode(Instant.EPOCH)).isEqualTo("0x0000000000000000"); + assertThat(encode(Instant.ofEpochMilli(128))).isEqualTo("0x0000000000000080"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x0000000000000000").toEpochMilli()).isEqualTo(0); + assertThat(decode("0x0000000000000080").toEpochMilli()).isEqualTo(128); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_not_enough_bytes() { + decode("0x0000"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x0000000000000000" + "0000"); + } + + @Test + public void should_format() { + // No need to test various values because the codec delegates directly to SimpleDateFormat, + // which we assume does its job correctly. + assertThat(format(Instant.EPOCH)).isEqualTo("'1970-01-01T00:00:00.000Z'"); + assertThat(format(Instant.parse("2018-08-16T15:59:34.123Z"))) + .isEqualTo("'2018-08-16T15:59:34.123Z'"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @DataProvider + public static Iterable timeZones() { + return Lists.newArrayList( + ZoneId.systemDefault(), + ZoneOffset.UTC, + ZoneOffset.ofHoursMinutes(3, 30), + ZoneId.of("Europe/Paris"), + ZoneId.of("GMT+7")); + } + + @Test + @UseDataProvider("timeZones") + public void should_parse(ZoneId defaultTimeZone) { + TimestampCodec codec = new TimestampCodec(defaultTimeZone); + + // Raw numbers + assertThat(codec.parse("'0'")).isEqualTo(Instant.EPOCH); + assertThat(codec.parse("'-1'")).isEqualTo(Instant.EPOCH.minusMillis(1)); + assertThat(codec.parse("1534463100000")).isEqualTo(Instant.ofEpochMilli(1534463100000L)); + + // Date formats + Instant expected; + + // date without time, without time zone + expected = LocalDate.parse("2017-01-01").atStartOfDay().atZone(defaultTimeZone).toInstant(); + assertThat(codec.parse("'2017-01-01'")).isEqualTo(expected); + + // date without time, with time zone + expected = LocalDate.parse("2018-08-16").atStartOfDay().atZone(ofHours(2)).toInstant(); + assertThat(codec.parse("'2018-08-16+02'")).isEqualTo(expected); + assertThat(codec.parse("'2018-08-16+0200'")).isEqualTo(expected); + assertThat(codec.parse("'2018-08-16+02:00'")).isEqualTo(expected); + assertThat(codec.parse("'2018-08-16 CEST'")).isEqualTo(expected); + + // date with time, without time zone + expected = LocalDateTime.parse("2018-08-16T23:45").atZone(defaultTimeZone).toInstant(); + assertThat(codec.parse("'2018-08-16T23:45'")).isEqualTo(expected); + assertThat(codec.parse("'2018-08-16 23:45'")).isEqualTo(expected); + + // date with time + seconds, without time zone + expected = LocalDateTime.parse("2019-12-31T16:08:38").atZone(defaultTimeZone).toInstant(); + assertThat(codec.parse("'2019-12-31T16:08:38'")).isEqualTo(expected); + assertThat(codec.parse("'2019-12-31 16:08:38'")).isEqualTo(expected); + + // date with time + seconds + milliseconds, without time zone + expected = LocalDateTime.parse("1950-02-28T12:00:59.230").atZone(defaultTimeZone).toInstant(); + assertThat(codec.parse("'1950-02-28T12:00:59.230'")).isEqualTo(expected); + assertThat(codec.parse("'1950-02-28 12:00:59.230'")).isEqualTo(expected); + + // date with time, with time zone + expected = ZonedDateTime.parse("1973-06-23T23:59:00.000+01:00").toInstant(); + assertThat(codec.parse("'1973-06-23T23:59+01'")).isEqualTo(expected); + assertThat(codec.parse("'1973-06-23T23:59+0100'")).isEqualTo(expected); + assertThat(codec.parse("'1973-06-23T23:59+01:00'")).isEqualTo(expected); + assertThat(codec.parse("'1973-06-23T23:59 CET'")).isEqualTo(expected); + assertThat(codec.parse("'1973-06-23 23:59+01'")).isEqualTo(expected); + assertThat(codec.parse("'1973-06-23 23:59+0100'")).isEqualTo(expected); + assertThat(codec.parse("'1973-06-23 23:59+01:00'")).isEqualTo(expected); + assertThat(codec.parse("'1973-06-23 23:59 CET'")).isEqualTo(expected); + + // date with time + seconds, with time zone + expected = ZonedDateTime.parse("1980-01-01T23:59:59.000-08:00").toInstant(); + assertThat(codec.parse("'1980-01-01T23:59:59-08'")).isEqualTo(expected); + assertThat(codec.parse("'1980-01-01T23:59:59-0800'")).isEqualTo(expected); + assertThat(codec.parse("'1980-01-01T23:59:59-08:00'")).isEqualTo(expected); + assertThat(codec.parse("'1980-01-01T23:59:59 PST'")).isEqualTo(expected); + assertThat(codec.parse("'1980-01-01 23:59:59-08'")).isEqualTo(expected); + assertThat(codec.parse("'1980-01-01 23:59:59-0800'")).isEqualTo(expected); + assertThat(codec.parse("'1980-01-01 23:59:59-08:00'")).isEqualTo(expected); + assertThat(codec.parse("'1980-01-01 23:59:59 PST'")).isEqualTo(expected); + + // date with time + seconds + milliseconds, with time zone + expected = ZonedDateTime.parse("1999-12-31T23:59:59.999+00:00").toInstant(); + assertThat(codec.parse("'1999-12-31T23:59:59.999+00'")).isEqualTo(expected); + assertThat(codec.parse("'1999-12-31T23:59:59.999+0000'")).isEqualTo(expected); + assertThat(codec.parse("'1999-12-31T23:59:59.999+00:00'")).isEqualTo(expected); + assertThat(codec.parse("'1999-12-31T23:59:59.999 UTC'")).isEqualTo(expected); + assertThat(codec.parse("'1999-12-31 23:59:59.999+00'")).isEqualTo(expected); + assertThat(codec.parse("'1999-12-31 23:59:59.999+0000'")).isEqualTo(expected); + assertThat(codec.parse("'1999-12-31 23:59:59.999+00:00'")).isEqualTo(expected); + assertThat(codec.parse("'1999-12-31 23:59:59.999 UTC'")).isEqualTo(expected); + + assertThat(codec.parse("NULL")).isNull(); + assertThat(codec.parse("null")).isNull(); + assertThat(codec.parse("")).isNull(); + assertThat(codec.parse(null)).isNull(); + } + + @Test + public void should_fail_to_parse_invalid_input() { + assertThatThrownBy(() -> parse("not a timestamp")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Alphanumeric timestamp literal must be quoted: \"not a timestamp\""); + assertThatThrownBy(() -> parse("'not a timestamp'")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse timestamp value from \"'not a timestamp'\""); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(Instant.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(Instant.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(Instant.EPOCH)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TinyIntCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TinyIntCodecTest.java new file mode 100644 index 00000000000..ae31be9dc42 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TinyIntCodecTest.java @@ -0,0 +1,90 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import org.junit.Test; + +public class TinyIntCodecTest extends CodecTestBase { + + public TinyIntCodecTest() { + this.codec = TypeCodecs.TINYINT; + } + + @Test + public void should_encode() { + // Our codec relies on the JDK's ByteBuffer API. We're not testing the JDK, so no need to try + // a thousand different values. + assertThat(encode((byte) 0)).isEqualTo("0x00"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x00")).isEqualTo((byte) 0); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x0000"); + } + + @Test + public void should_format() { + assertThat(format((byte) 0)).isEqualTo("0"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("0")).isEqualTo((byte) 0); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a tinyint"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(Byte.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(byte.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(Byte.class)).isTrue(); + assertThat(codec.accepts(byte.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(((byte) 123))).isTrue(); + assertThat(codec.accepts(Byte.MIN_VALUE)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TupleCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TupleCodecTest.java new file mode 100644 index 00000000000..f7d609ea967 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/TupleCodecTest.java @@ -0,0 +1,188 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveIntCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.data.DefaultTupleValue; +import com.datastax.oss.driver.internal.core.type.DefaultTupleType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.util.Bytes; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class TupleCodecTest extends CodecTestBase { + + @Mock private AttachmentPoint attachmentPoint; + @Mock private CodecRegistry codecRegistry; + private PrimitiveIntCodec intCodec; + private TypeCodec doubleCodec; + private TypeCodec textCodec; + + private TupleType tupleType; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(attachmentPoint.getCodecRegistry()).thenReturn(codecRegistry); + when(attachmentPoint.getProtocolVersion()).thenReturn(ProtocolVersion.DEFAULT); + + intCodec = spy(TypeCodecs.INT); + doubleCodec = spy(TypeCodecs.DOUBLE); + textCodec = spy(TypeCodecs.TEXT); + + // Called by the getters/setters + when(codecRegistry.codecFor(DataTypes.INT, Integer.class)).thenAnswer(i -> intCodec); + when(codecRegistry.codecFor(DataTypes.DOUBLE, Double.class)).thenAnswer(i -> doubleCodec); + when(codecRegistry.codecFor(DataTypes.TEXT, String.class)).thenAnswer(i -> textCodec); + + // Called by format/parse + when(codecRegistry.codecFor(DataTypes.INT)).thenAnswer(i -> intCodec); + when(codecRegistry.codecFor(DataTypes.DOUBLE)).thenAnswer(i -> doubleCodec); + when(codecRegistry.codecFor(DataTypes.TEXT)).thenAnswer(i -> textCodec); + + tupleType = + new DefaultTupleType( + ImmutableList.of(DataTypes.INT, DataTypes.DOUBLE, DataTypes.TEXT), attachmentPoint); + + codec = TypeCodecs.tupleOf(tupleType); + } + + @Test + public void should_encode_null_tuple() { + assertThat(encode(null)).isNull(); + } + + @Test + public void should_encode_tuple() { + TupleValue tuple = tupleType.newValue(); + tuple = tuple.setInt(0, 1); + tuple = tuple.setToNull(1); + tuple = tuple.setString(2, "a"); + + assertThat(encode(tuple)) + .isEqualTo( + "0x" + + ("00000004" + "00000001") // size and contents of field 0 + + "ffffffff" // null field 1 + + ("00000001" + "61") // size and contents of field 2 + ); + + verify(intCodec).encodePrimitive(1, ProtocolVersion.DEFAULT); + // null values are handled directly in the tuple codec, without calling the child codec: + verifyZeroInteractions(doubleCodec); + verify(textCodec).encode("a", ProtocolVersion.DEFAULT); + } + + @Test + public void should_decode_null_tuple() { + assertThat(decode(null)).isNull(); + } + + @Test + public void should_decode_tuple() { + TupleValue tuple = decode("0x" + ("00000004" + "00000001") + "ffffffff" + ("00000001" + "61")); + + assertThat(tuple.getInt(0)).isEqualTo(1); + assertThat(tuple.isNull(1)).isTrue(); + assertThat(tuple.getString(2)).isEqualTo("a"); + + verify(intCodec).decodePrimitive(Bytes.fromHexString("0x00000001"), ProtocolVersion.DEFAULT); + verifyZeroInteractions(doubleCodec); + verify(textCodec).decode(Bytes.fromHexString("0x61"), ProtocolVersion.DEFAULT); + } + + @Test + public void should_format_null_tuple() { + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_format_tuple() { + TupleValue tuple = tupleType.newValue(); + tuple = tuple.setInt(0, 1); + tuple = tuple.setToNull(1); + tuple = tuple.setString(2, "a"); + + assertThat(format(tuple)).isEqualTo("(1,NULL,'a')"); + + verify(intCodec).format(1); + verify(doubleCodec).format(null); + verify(textCodec).format("a"); + } + + @Test + public void should_parse_null_tuple() { + assertThat(parse(null)).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("NULL")).isNull(); + } + + @Test + public void should_parse_tuple() { + TupleValue tuple = parse("(1,NULL,'a')"); + + assertThat(tuple.getInt(0)).isEqualTo(1); + assertThat(tuple.isNull(1)).isTrue(); + assertThat(tuple.getString(2)).isEqualTo("a"); + + verify(intCodec).parse("1"); + verify(doubleCodec).parse("NULL"); + verify(textCodec).parse("'a'"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a tuple"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(TupleValue.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(DefaultTupleValue.class))) + .isFalse(); // covariance not allowed + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(TupleValue.class)).isTrue(); + assertThat(codec.accepts(DefaultTupleValue.class)).isFalse(); // covariance not allowed + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(tupleType.newValue())).isTrue(); + assertThat(codec.accepts(new DefaultTupleValue(tupleType))).isTrue(); // covariance allowed + assertThat(codec.accepts("not a tuple")).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/UdtCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/UdtCodecTest.java new file mode 100644 index 00000000000..5947cfffef3 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/UdtCodecTest.java @@ -0,0 +1,197 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.detach.AttachmentPoint; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.PrimitiveIntCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.data.DefaultUdtValue; +import com.datastax.oss.driver.internal.core.type.DefaultUserDefinedType; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.protocol.internal.util.Bytes; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class UdtCodecTest extends CodecTestBase { + + @Mock private AttachmentPoint attachmentPoint; + @Mock private CodecRegistry codecRegistry; + private PrimitiveIntCodec intCodec; + private TypeCodec doubleCodec; + private TypeCodec textCodec; + + private UserDefinedType userType; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(attachmentPoint.getCodecRegistry()).thenReturn(codecRegistry); + when(attachmentPoint.getProtocolVersion()).thenReturn(ProtocolVersion.DEFAULT); + + intCodec = spy(TypeCodecs.INT); + doubleCodec = spy(TypeCodecs.DOUBLE); + textCodec = spy(TypeCodecs.TEXT); + + // Called by the getters/setters + when(codecRegistry.codecFor(DataTypes.INT, Integer.class)).thenAnswer(i -> intCodec); + when(codecRegistry.codecFor(DataTypes.DOUBLE, Double.class)).thenAnswer(i -> doubleCodec); + when(codecRegistry.codecFor(DataTypes.TEXT, String.class)).thenAnswer(i -> textCodec); + + // Called by format/parse + when(codecRegistry.codecFor(DataTypes.INT)).thenAnswer(i -> intCodec); + when(codecRegistry.codecFor(DataTypes.DOUBLE)).thenAnswer(i -> doubleCodec); + when(codecRegistry.codecFor(DataTypes.TEXT)).thenAnswer(i -> textCodec); + + userType = + new DefaultUserDefinedType( + CqlIdentifier.fromInternal("ks"), + CqlIdentifier.fromInternal("type"), + false, + ImmutableList.of( + CqlIdentifier.fromInternal("field1"), + CqlIdentifier.fromInternal("field2"), + CqlIdentifier.fromInternal("field3")), + ImmutableList.of(DataTypes.INT, DataTypes.DOUBLE, DataTypes.TEXT), + attachmentPoint); + + codec = TypeCodecs.udtOf(userType); + } + + @Test + public void should_encode_null_udt() { + assertThat(encode(null)).isNull(); + } + + @Test + public void should_encode_udt() { + UdtValue udt = userType.newValue(); + udt = udt.setInt("field1", 1); + udt = udt.setToNull("field2"); + udt = udt.setString("field3", "a"); + + assertThat(encode(udt)) + .isEqualTo( + "0x" + + ("00000004" + "00000001") // size and contents of field 0 + + "ffffffff" // null field 1 + + ("00000001" + "61") // size and contents of field 2 + ); + + verify(intCodec).encodePrimitive(1, ProtocolVersion.DEFAULT); + // null values are handled directly in the udt codec, without calling the child codec: + verifyZeroInteractions(doubleCodec); + verify(textCodec).encode("a", ProtocolVersion.DEFAULT); + } + + @Test + public void should_decode_null_udt() { + assertThat(decode(null)).isNull(); + } + + @Test + public void should_decode_udt() { + UdtValue udt = decode("0x" + ("00000004" + "00000001") + "ffffffff" + ("00000001" + "61")); + + assertThat(udt.getInt(0)).isEqualTo(1); + assertThat(udt.isNull(1)).isTrue(); + assertThat(udt.getString(2)).isEqualTo("a"); + + verify(intCodec).decodePrimitive(Bytes.fromHexString("0x00000001"), ProtocolVersion.DEFAULT); + verifyZeroInteractions(doubleCodec); + verify(textCodec).decode(Bytes.fromHexString("0x61"), ProtocolVersion.DEFAULT); + } + + @Test + public void should_format_null_udt() { + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_format_udt() { + UdtValue udt = userType.newValue(); + udt = udt.setInt(0, 1); + udt = udt.setToNull(1); + udt = udt.setString(2, "a"); + + assertThat(format(udt)).isEqualTo("{field1:1,field2:NULL,field3:'a'}"); + + verify(intCodec).format(1); + verify(doubleCodec).format(null); + verify(textCodec).format("a"); + } + + @Test + public void should_parse_null_udt() { + assertThat(parse(null)).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("NULL")).isNull(); + } + + @Test + public void should_parse_udt() { + UdtValue udt = parse("{field1:1,field2:NULL,field3:'a'}"); + + assertThat(udt.getInt(0)).isEqualTo(1); + assertThat(udt.isNull(1)).isTrue(); + assertThat(udt.getString(2)).isEqualTo("a"); + + verify(intCodec).parse("1"); + verify(doubleCodec).parse("NULL"); + verify(textCodec).parse("'a'"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a udt"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(UdtValue.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(DefaultUdtValue.class))) + .isFalse(); // covariance not allowed + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(UdtValue.class)).isTrue(); + assertThat(codec.accepts(DefaultUdtValue.class)).isFalse(); // covariance not allowed + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(userType.newValue())).isTrue(); + assertThat(codec.accepts(new DefaultUdtValue(userType))).isTrue(); // covariance allowed + assertThat(codec.accepts("not a udt")).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/UuidCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/UuidCodecTest.java new file mode 100644 index 00000000000..16baada6810 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/UuidCodecTest.java @@ -0,0 +1,94 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import java.util.UUID; +import org.junit.Test; + +public class UuidCodecTest extends CodecTestBase { + private static final UUID MOCK_UUID = new UUID(2L, 1L); + + public UuidCodecTest() { + this.codec = TypeCodecs.UUID; + } + + @Test + public void should_encode() { + assertThat(encode(MOCK_UUID)).isEqualTo("0x00000000000000020000000000000001"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + UUID decoded = decode("0x00000000000000020000000000000001"); + assertThat(decoded.getMostSignificantBits()).isEqualTo(2L); + assertThat(decoded.getLeastSignificantBits()).isEqualTo(1L); + + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_not_enough_bytes() { + decode("0x0000"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + decode("0x00000000000000020000000000000001" + "0000"); + } + + @Test + public void should_format() { + assertThat(format(MOCK_UUID)).isEqualTo("00000000-0000-0002-0000-000000000001"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("00000000-0000-0002-0000-000000000001")).isEqualTo(MOCK_UUID); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a uuid"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(UUID.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(UUID.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(MOCK_UUID)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/VarintCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/VarintCodecTest.java new file mode 100644 index 00000000000..e52dd93919c --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/VarintCodecTest.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import java.math.BigInteger; +import org.junit.Test; + +public class VarintCodecTest extends CodecTestBase { + + public VarintCodecTest() { + this.codec = TypeCodecs.VARINT; + } + + @Test + public void should_encode() { + assertThat(encode(BigInteger.ONE)).isEqualTo("0x01"); + assertThat(encode(BigInteger.valueOf(128))).isEqualTo("0x0080"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + assertThat(decode("0x01")).isEqualTo(BigInteger.ONE); + assertThat(decode("0x0080")).isEqualTo(BigInteger.valueOf(128)); + assertThat(decode("0x")).isNull(); + assertThat(decode(null)).isNull(); + } + + @Test + public void should_format() { + assertThat(format(BigInteger.ONE)).isEqualTo("1"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + public void should_parse() { + assertThat(parse("1")).isEqualTo(BigInteger.ONE); + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_parse_invalid_input() { + parse("not a varint"); + } + + @Test + public void should_accept_generic_type() { + assertThat(codec.accepts(GenericType.of(BigInteger.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + assertThat(codec.accepts(BigInteger.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + assertThat(codec.accepts(BigInteger.ONE)).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/ZonedTimestampCodecTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/ZonedTimestampCodecTest.java new file mode 100644 index 00000000000..5fb73d0ec76 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/ZonedTimestampCodecTest.java @@ -0,0 +1,189 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec; + +import static java.time.ZoneOffset.ofHours; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(DataProviderRunner.class) +public class ZonedTimestampCodecTest extends CodecTestBase { + + @Test + @UseDataProvider(value = "timeZones", location = TimestampCodecTest.class) + public void should_encode(ZoneId timeZone) { + codec = TypeCodecs.zonedTimestampAt(timeZone); + assertThat(encode(Instant.EPOCH.atZone(timeZone))).isEqualTo("0x0000000000000000"); + assertThat(encode(Instant.ofEpochMilli(128).atZone(timeZone))).isEqualTo("0x0000000000000080"); + assertThat(encode(null)).isNull(); + } + + @Test + public void should_decode() { + codec = TypeCodecs.ZONED_TIMESTAMP_UTC; + assertThat(decode("0x0000000000000000").toInstant().toEpochMilli()).isEqualTo(0); + assertThat(decode("0x0000000000000080").toInstant().toEpochMilli()).isEqualTo(128); + assertThat(decode(null)).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_not_enough_bytes() { + codec = TypeCodecs.ZONED_TIMESTAMP_SYSTEM; + decode("0x0000"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_decode_if_too_many_bytes() { + codec = TypeCodecs.ZONED_TIMESTAMP_SYSTEM; + decode("0x0000000000000000" + "0000"); + } + + @Test + public void should_format() { + codec = TypeCodecs.zonedTimestampAt(ZoneOffset.ofHours(2)); + // No need to test various values because the codec delegates directly to SimpleDateFormat, + // which we assume does its job correctly. + assertThat(format(Instant.EPOCH.atZone(ZoneOffset.UTC))) + .isEqualTo("'1970-01-01T02:00:00.000+02:00'"); + assertThat(format(ZonedDateTime.parse("2018-08-16T15:59:34.123Z"))) + .isEqualTo("'2018-08-16T17:59:34.123+02:00'"); + assertThat(format(null)).isEqualTo("NULL"); + } + + @Test + @UseDataProvider(value = "timeZones", location = TimestampCodecTest.class) + public void should_parse(ZoneId timeZone) { + codec = TypeCodecs.zonedTimestampAt(timeZone); + + // Raw numbers + assertThat(parse("'0'")).isEqualTo(Instant.EPOCH.atZone(timeZone)); + assertThat(parse("'-1'")).isEqualTo(Instant.EPOCH.minusMillis(1).atZone(timeZone)); + assertThat(parse("1534463100000")) + .isEqualTo(Instant.ofEpochMilli(1534463100000L).atZone(timeZone)); + + // Date formats + ZonedDateTime expected; + + // date without time, without time zone + expected = LocalDate.parse("2017-01-01").atStartOfDay().atZone(timeZone); + assertThat(parse("'2017-01-01'")).isEqualTo(expected); + + // date without time, with time zone + expected = LocalDate.parse("2018-08-16").atStartOfDay().atZone(ofHours(2)); + assertThat(parse("'2018-08-16+02'")).isEqualTo(expected); + assertThat(parse("'2018-08-16+0200'")).isEqualTo(expected); + assertThat(parse("'2018-08-16+02:00'")).isEqualTo(expected); + assertThat(parse("'2018-08-16 CEST'")).isEqualTo(expected); + + // date with time, without time zone + expected = LocalDateTime.parse("2018-08-16T23:45").atZone(timeZone); + assertThat(parse("'2018-08-16T23:45'")).isEqualTo(expected); + assertThat(parse("'2018-08-16 23:45'")).isEqualTo(expected); + + // date with time + seconds, without time zone + expected = LocalDateTime.parse("2019-12-31T16:08:38").atZone(timeZone); + assertThat(parse("'2019-12-31T16:08:38'")).isEqualTo(expected); + assertThat(parse("'2019-12-31 16:08:38'")).isEqualTo(expected); + + // date with time + seconds + milliseconds, without time zone + expected = LocalDateTime.parse("1950-02-28T12:00:59.230").atZone(timeZone); + assertThat(parse("'1950-02-28T12:00:59.230'")).isEqualTo(expected); + assertThat(parse("'1950-02-28 12:00:59.230'")).isEqualTo(expected); + + // date with time, with time zone + expected = ZonedDateTime.parse("1973-06-23T23:59:00.000+01:00"); + assertThat(parse("'1973-06-23T23:59+01'")).isEqualTo(expected); + assertThat(parse("'1973-06-23T23:59+0100'")).isEqualTo(expected); + assertThat(parse("'1973-06-23T23:59+01:00'")).isEqualTo(expected); + assertThat(parse("'1973-06-23T23:59 CET'")).isEqualTo(expected); + assertThat(parse("'1973-06-23 23:59+01'")).isEqualTo(expected); + assertThat(parse("'1973-06-23 23:59+0100'")).isEqualTo(expected); + assertThat(parse("'1973-06-23 23:59+01:00'")).isEqualTo(expected); + assertThat(parse("'1973-06-23 23:59 CET'")).isEqualTo(expected); + + // date with time + seconds, with time zone + expected = ZonedDateTime.parse("1980-01-01T23:59:59.000-08:00"); + assertThat(parse("'1980-01-01T23:59:59-08'")).isEqualTo(expected); + assertThat(parse("'1980-01-01T23:59:59-0800'")).isEqualTo(expected); + assertThat(parse("'1980-01-01T23:59:59-08:00'")).isEqualTo(expected); + assertThat(parse("'1980-01-01T23:59:59 PST'")).isEqualTo(expected); + assertThat(parse("'1980-01-01 23:59:59-08'")).isEqualTo(expected); + assertThat(parse("'1980-01-01 23:59:59-0800'")).isEqualTo(expected); + assertThat(parse("'1980-01-01 23:59:59-08:00'")).isEqualTo(expected); + assertThat(parse("'1980-01-01 23:59:59 PST'")).isEqualTo(expected); + + // date with time + seconds + milliseconds, with time zone + expected = ZonedDateTime.parse("1999-12-31T23:59:59.999+00:00"); + assertThat(parse("'1999-12-31T23:59:59.999+00'")).isEqualTo(expected); + assertThat(parse("'1999-12-31T23:59:59.999+0000'")).isEqualTo(expected); + assertThat(parse("'1999-12-31T23:59:59.999+00:00'")).isEqualTo(expected); + assertThat(parse("'1999-12-31T23:59:59.999 UTC'")).isEqualTo(expected); + assertThat(parse("'1999-12-31 23:59:59.999+00'")).isEqualTo(expected); + assertThat(parse("'1999-12-31 23:59:59.999+0000'")).isEqualTo(expected); + assertThat(parse("'1999-12-31 23:59:59.999+00:00'")).isEqualTo(expected); + assertThat(parse("'1999-12-31 23:59:59.999 UTC'")).isEqualTo(expected); + + assertThat(parse("NULL")).isNull(); + assertThat(parse("null")).isNull(); + assertThat(parse("")).isNull(); + assertThat(parse(null)).isNull(); + } + + @Test + public void should_fail_to_parse_invalid_input() { + codec = new ZonedTimestampCodec(); + assertThatThrownBy(() -> parse("not a timestamp")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Alphanumeric timestamp literal must be quoted: \"not a timestamp\""); + assertThatThrownBy(() -> parse("'not a timestamp'")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Cannot parse timestamp value from \"'not a timestamp'\""); + } + + @Test + public void should_accept_generic_type() { + codec = new ZonedTimestampCodec(); + assertThat(codec.accepts(GenericType.of(ZonedDateTime.class))).isTrue(); + assertThat(codec.accepts(GenericType.of(Integer.class))).isFalse(); + } + + @Test + public void should_accept_raw_type() { + codec = new ZonedTimestampCodec(); + assertThat(codec.accepts(ZonedDateTime.class)).isTrue(); + assertThat(codec.accepts(Integer.class)).isFalse(); + } + + @Test + public void should_accept_object() { + codec = new ZonedTimestampCodec(); + assertThat(codec.accepts(ZonedDateTime.now())).isTrue(); + assertThat(codec.accepts(Integer.MIN_VALUE)).isFalse(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/registry/CachingCodecRegistryTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/registry/CachingCodecRegistryTest.java new file mode 100644 index 00000000000..72056f6860a --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/type/codec/registry/CachingCodecRegistryTest.java @@ -0,0 +1,908 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.type.codec.registry; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verifyZeroInteractions; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.data.CqlDuration; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.ListType; +import com.datastax.oss.driver.api.core.type.MapType; +import com.datastax.oss.driver.api.core.type.SetType; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.TypeCodecs; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.type.UserDefinedTypeBuilder; +import com.datastax.oss.driver.internal.core.type.codec.CqlIntToStringCodec; +import com.datastax.oss.driver.internal.core.type.codec.ListCodec; +import com.datastax.oss.driver.internal.core.type.codec.registry.CachingCodecRegistryTest.TestCachingCodecRegistry.MockCache; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import com.datastax.oss.driver.shaded.guava.common.collect.Lists; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.Period; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class CachingCodecRegistryTest { + + @Mock private MockCache mockCache; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void should_find_primitive_codecs_for_types() { + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + checkPrimitiveMappings(registry, TypeCodecs.BOOLEAN); + checkPrimitiveMappings(registry, TypeCodecs.TINYINT); + checkPrimitiveMappings(registry, TypeCodecs.DOUBLE); + checkPrimitiveMappings(registry, TypeCodecs.COUNTER); + checkPrimitiveMappings(registry, TypeCodecs.FLOAT); + checkPrimitiveMappings(registry, TypeCodecs.INT); + checkPrimitiveMappings(registry, TypeCodecs.BIGINT); + checkPrimitiveMappings(registry, TypeCodecs.SMALLINT); + checkPrimitiveMappings(registry, TypeCodecs.TIMESTAMP); + checkPrimitiveMappings(registry, TypeCodecs.DATE); + checkPrimitiveMappings(registry, TypeCodecs.TIME); + checkPrimitiveMappings(registry, TypeCodecs.BLOB); + checkPrimitiveMappings(registry, TypeCodecs.TEXT); + checkPrimitiveMappings(registry, TypeCodecs.ASCII); + checkPrimitiveMappings(registry, TypeCodecs.VARINT); + checkPrimitiveMappings(registry, TypeCodecs.DECIMAL); + checkPrimitiveMappings(registry, TypeCodecs.UUID); + checkPrimitiveMappings(registry, TypeCodecs.TIMEUUID); + checkPrimitiveMappings(registry, TypeCodecs.INET); + checkPrimitiveMappings(registry, TypeCodecs.DURATION); + // Primitive mappings never hit the cache + verifyZeroInteractions(mockCache); + } + + private void checkPrimitiveMappings(TestCachingCodecRegistry registry, TypeCodec codec) { + DataType cqlType = codec.getCqlType(); + GenericType javaType = codec.getJavaType(); + + assertThat(registry.codecFor(cqlType, javaType)).isSameAs(codec); + assertThat(registry.codecFor(cqlType)).isSameAs(codec); + + assertThat(javaType.__getToken().getType()).isInstanceOf(Class.class); + Class javaClass = (Class) javaType.__getToken().getType(); + assertThat(registry.codecFor(cqlType, javaClass)).isSameAs(codec); + } + + @Test + public void should_find_primitive_codecs_for_value() throws Exception { + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + assertThat(registry.codecFor(true)).isEqualTo(TypeCodecs.BOOLEAN); + assertThat(registry.codecFor((byte) 0)).isEqualTo(TypeCodecs.TINYINT); + assertThat(registry.codecFor(0.0)).isEqualTo(TypeCodecs.DOUBLE); + assertThat(registry.codecFor(0.0f)).isEqualTo(TypeCodecs.FLOAT); + assertThat(registry.codecFor(0)).isEqualTo(TypeCodecs.INT); + assertThat(registry.codecFor(0L)).isEqualTo(TypeCodecs.BIGINT); + assertThat(registry.codecFor((short) 0)).isEqualTo(TypeCodecs.SMALLINT); + assertThat(registry.codecFor(Instant.EPOCH)).isEqualTo(TypeCodecs.TIMESTAMP); + assertThat(registry.codecFor(LocalDate.MIN)).isEqualTo(TypeCodecs.DATE); + assertThat(registry.codecFor(LocalTime.MIDNIGHT)).isEqualTo(TypeCodecs.TIME); + assertThat(registry.codecFor(ByteBuffer.allocate(0))).isEqualTo(TypeCodecs.BLOB); + assertThat(registry.codecFor("")).isEqualTo(TypeCodecs.TEXT); + assertThat(registry.codecFor(BigInteger.ONE)).isEqualTo(TypeCodecs.VARINT); + assertThat(registry.codecFor(BigDecimal.ONE)).isEqualTo(TypeCodecs.DECIMAL); + assertThat(registry.codecFor(new UUID(2L, 1L))).isEqualTo(TypeCodecs.UUID); + assertThat(registry.codecFor(InetAddress.getByName("127.0.0.1"))).isEqualTo(TypeCodecs.INET); + assertThat(registry.codecFor(CqlDuration.newInstance(1, 2, 3))).isEqualTo(TypeCodecs.DURATION); + verifyZeroInteractions(mockCache); + } + + @Test + public void should_find_primitive_codecs_for_cql_type_and_value() throws Exception { + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + assertThat(registry.codecFor(DataTypes.BOOLEAN, true)).isEqualTo(TypeCodecs.BOOLEAN); + assertThat(registry.codecFor(DataTypes.TINYINT, (byte) 0)).isEqualTo(TypeCodecs.TINYINT); + assertThat(registry.codecFor(DataTypes.DOUBLE, 0.0)).isEqualTo(TypeCodecs.DOUBLE); + assertThat(registry.codecFor(DataTypes.FLOAT, 0.0f)).isEqualTo(TypeCodecs.FLOAT); + assertThat(registry.codecFor(DataTypes.INT, 0)).isEqualTo(TypeCodecs.INT); + assertThat(registry.codecFor(DataTypes.BIGINT, 0L)).isEqualTo(TypeCodecs.BIGINT); + assertThat(registry.codecFor(DataTypes.SMALLINT, (short) 0)).isEqualTo(TypeCodecs.SMALLINT); + assertThat(registry.codecFor(DataTypes.TIMESTAMP, Instant.EPOCH)) + .isEqualTo(TypeCodecs.TIMESTAMP); + assertThat(registry.codecFor(DataTypes.DATE, LocalDate.MIN)).isEqualTo(TypeCodecs.DATE); + assertThat(registry.codecFor(DataTypes.TIME, LocalTime.MIDNIGHT)).isEqualTo(TypeCodecs.TIME); + assertThat(registry.codecFor(DataTypes.BLOB, ByteBuffer.allocate(0))) + .isEqualTo(TypeCodecs.BLOB); + assertThat(registry.codecFor(DataTypes.TEXT, "")).isEqualTo(TypeCodecs.TEXT); + assertThat(registry.codecFor(DataTypes.VARINT, BigInteger.ONE)).isEqualTo(TypeCodecs.VARINT); + assertThat(registry.codecFor(DataTypes.DECIMAL, BigDecimal.ONE)).isEqualTo(TypeCodecs.DECIMAL); + assertThat(registry.codecFor(DataTypes.UUID, new UUID(2L, 1L))).isEqualTo(TypeCodecs.UUID); + assertThat(registry.codecFor(DataTypes.INET, InetAddress.getByName("127.0.0.1"))) + .isEqualTo(TypeCodecs.INET); + assertThat(registry.codecFor(DataTypes.DURATION, CqlDuration.newInstance(1, 2, 3))) + .isEqualTo(TypeCodecs.DURATION); + verifyZeroInteractions(mockCache); + } + + @Test + public void should_find_user_codec_for_built_in_java_type() { + // int and String are built-in types, but int <-> String is not a built-in mapping + CqlIntToStringCodec intToStringCodec1 = new CqlIntToStringCodec(); + // register a second codec to also check that the first one is preferred + CqlIntToStringCodec intToStringCodec2 = new CqlIntToStringCodec(); + TestCachingCodecRegistry registry = + new TestCachingCodecRegistry(mockCache, intToStringCodec1, intToStringCodec2); + + // When the mapping is not ambiguous, the user type should be returned + assertThat(registry.codecFor(DataTypes.INT, GenericType.STRING)).isSameAs(intToStringCodec1); + assertThat(registry.codecFor(DataTypes.INT, String.class)).isSameAs(intToStringCodec1); + assertThat(registry.codecFor(DataTypes.INT, "")).isSameAs(intToStringCodec1); + + // When there is an ambiguity with a built-in codec, the built-in codec should have priority + assertThat(registry.codecFor(DataTypes.INT)).isSameAs(TypeCodecs.INT); + assertThat(registry.codecFor("")).isSameAs(TypeCodecs.TEXT); + + verifyZeroInteractions(mockCache); + } + + @Test + public void should_find_user_codec_for_custom_java_type() { + TextToPeriodCodec textToPeriodCodec1 = new TextToPeriodCodec(); + TextToPeriodCodec textToPeriodCodec2 = new TextToPeriodCodec(); + TestCachingCodecRegistry registry = + new TestCachingCodecRegistry(mockCache, textToPeriodCodec1, textToPeriodCodec2); + + assertThat(registry.codecFor(DataTypes.TEXT, GenericType.of(Period.class))) + .isSameAs(textToPeriodCodec1); + assertThat(registry.codecFor(DataTypes.TEXT, Period.class)).isSameAs(textToPeriodCodec1); + assertThat(registry.codecFor(DataTypes.TEXT, Period.ofDays(1))).isSameAs(textToPeriodCodec1); + // Now even the search by Java value only is not ambiguous + assertThat(registry.codecFor(Period.ofDays(1))).isSameAs(textToPeriodCodec1); + + // The search by CQL type only still returns the built-in codec + assertThat(registry.codecFor(DataTypes.TEXT)).isSameAs(TypeCodecs.TEXT); + + verifyZeroInteractions(mockCache); + } + + @Test + public void should_create_list_codec_for_cql_and_java_types() { + ListType cqlType = DataTypes.listOf(DataTypes.listOf(DataTypes.INT)); + GenericType>> javaType = new GenericType>>() {}; + List> value = ImmutableList.of(ImmutableList.of(1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(cqlType, javaType); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + // Cache lookup for the codec, and recursively for its subcodec + inOrder.verify(mockCache).lookup(cqlType, javaType, false); + inOrder + .verify(mockCache) + .lookup(DataTypes.listOf(DataTypes.INT), GenericType.listOf(GenericType.INTEGER), false); + } + + @Test + public void should_create_list_codec_for_cql_type() { + ListType cqlType = DataTypes.listOf(DataTypes.listOf(DataTypes.INT)); + GenericType>> javaType = new GenericType>>() {}; + List> value = ImmutableList.of(ImmutableList.of(1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(cqlType); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, null, false); + inOrder.verify(mockCache).lookup(DataTypes.listOf(DataTypes.INT), null, false); + } + + @Test + public void should_create_list_codec_for_cql_type_and_java_value() { + ListType cqlType = DataTypes.listOf(DataTypes.listOf(DataTypes.INT)); + GenericType>> javaType = new GenericType>>() {}; + List> value = ImmutableList.of(ImmutableList.of(1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(cqlType, value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, javaType, true); + inOrder + .verify(mockCache) + .lookup(DataTypes.listOf(DataTypes.INT), GenericType.listOf(GenericType.INTEGER), true); + } + + @Test + public void should_create_list_codec_for_java_value() { + ListType cqlType = DataTypes.listOf(DataTypes.listOf(DataTypes.INT)); + GenericType>> javaType = new GenericType>>() {}; + List> value = ImmutableList.of(ImmutableList.of(1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(null, javaType, true); + inOrder.verify(mockCache).lookup(null, GenericType.listOf(GenericType.INTEGER), true); + } + + @Test + public void should_create_list_codec_for_java_value_when_first_element_is_a_subtype() + throws UnknownHostException { + ListType cqlType = DataTypes.listOf(DataTypes.INET); + GenericType> javaType = new GenericType>() {}; + InetAddress address = InetAddress.getByAddress(new byte[] {127, 0, 0, 1}); + // Because the actual implementation is a subclass, there is no exact match with the codec's + // declared type + assertThat(address).isInstanceOf(Inet4Address.class); + List value = ImmutableList.of(address); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec> codec = registry.codecFor(value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + + inOrder.verify(mockCache).lookup(null, GenericType.listOf(Inet4Address.class), true); + } + + @Test + public void should_create_set_codec_for_cql_and_java_types() { + SetType cqlType = DataTypes.setOf(DataTypes.setOf(DataTypes.INT)); + GenericType>> javaType = new GenericType>>() {}; + Set> value = ImmutableSet.of(ImmutableSet.of(1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(cqlType, javaType); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + // Cache lookup for the codec, and recursively for its subcodec + inOrder.verify(mockCache).lookup(cqlType, javaType, false); + inOrder + .verify(mockCache) + .lookup(DataTypes.setOf(DataTypes.INT), GenericType.setOf(GenericType.INTEGER), false); + } + + @Test + public void should_create_set_codec_for_cql_type() { + SetType cqlType = DataTypes.setOf(DataTypes.setOf(DataTypes.INT)); + GenericType>> javaType = new GenericType>>() {}; + Set> value = ImmutableSet.of(ImmutableSet.of(1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(cqlType); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, null, false); + inOrder.verify(mockCache).lookup(DataTypes.setOf(DataTypes.INT), null, false); + } + + @Test + public void should_create_set_codec_for_cql_type_and_java_value() { + SetType cqlType = DataTypes.setOf(DataTypes.setOf(DataTypes.INT)); + GenericType>> javaType = new GenericType>>() {}; + Set> value = ImmutableSet.of(ImmutableSet.of(1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(cqlType, value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, javaType, true); + inOrder + .verify(mockCache) + .lookup(DataTypes.setOf(DataTypes.INT), GenericType.setOf(GenericType.INTEGER), true); + } + + @Test + public void should_create_set_codec_for_java_value() { + SetType cqlType = DataTypes.setOf(DataTypes.setOf(DataTypes.INT)); + GenericType>> javaType = new GenericType>>() {}; + Set> value = ImmutableSet.of(ImmutableSet.of(1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(null, javaType, true); + inOrder.verify(mockCache).lookup(null, GenericType.setOf(GenericType.INTEGER), true); + } + + @Test + public void should_create_set_codec_for_java_value_when_first_element_is_a_subtype() + throws UnknownHostException { + SetType cqlType = DataTypes.setOf(DataTypes.INET); + GenericType> javaType = new GenericType>() {}; + InetAddress address = InetAddress.getByAddress(new byte[] {127, 0, 0, 1}); + // Because the actual implementation is a subclass, there is no exact match with the codec's + // declared type + assertThat(address).isInstanceOf(Inet4Address.class); + Set value = ImmutableSet.of(address); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec> codec = registry.codecFor(value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + + inOrder.verify(mockCache).lookup(null, GenericType.setOf(Inet4Address.class), true); + } + + @Test + public void should_create_map_codec_for_cql_and_java_types() { + MapType cqlType = DataTypes.mapOf(DataTypes.INT, DataTypes.mapOf(DataTypes.INT, DataTypes.INT)); + GenericType>> javaType = + new GenericType>>() {}; + Map> value = ImmutableMap.of(1, ImmutableMap.of(1, 1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(cqlType, javaType); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + // Cache lookup for the codec, and recursively for its subcodec + inOrder.verify(mockCache).lookup(cqlType, javaType, false); + inOrder + .verify(mockCache) + .lookup( + DataTypes.mapOf(DataTypes.INT, DataTypes.INT), + GenericType.mapOf(GenericType.INTEGER, GenericType.INTEGER), + false); + } + + @Test + public void should_create_map_codec_for_cql_type() { + MapType cqlType = DataTypes.mapOf(DataTypes.INT, DataTypes.mapOf(DataTypes.INT, DataTypes.INT)); + GenericType>> javaType = + new GenericType>>() {}; + Map> value = ImmutableMap.of(1, ImmutableMap.of(1, 1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(cqlType); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, null, false); + inOrder.verify(mockCache).lookup(DataTypes.mapOf(DataTypes.INT, DataTypes.INT), null, false); + } + + @Test + public void should_create_map_codec_for_java_type() { + MapType cqlType = DataTypes.mapOf(DataTypes.INT, DataTypes.mapOf(DataTypes.INT, DataTypes.INT)); + GenericType>> javaType = + new GenericType>>() {}; + Map> value = ImmutableMap.of(1, ImmutableMap.of(1, 1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(javaType); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(null, javaType, false); + inOrder.verify(mockCache).lookup(null, new GenericType>() {}, false); + } + + @Test + public void should_create_map_codec_for_cql_type_and_java_value() { + MapType cqlType = DataTypes.mapOf(DataTypes.INT, DataTypes.mapOf(DataTypes.INT, DataTypes.INT)); + GenericType>> javaType = + new GenericType>>() {}; + Map> value = ImmutableMap.of(1, ImmutableMap.of(1, 1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(cqlType, value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, javaType, true); + inOrder + .verify(mockCache) + .lookup( + DataTypes.mapOf(DataTypes.INT, DataTypes.INT), + GenericType.mapOf(GenericType.INTEGER, GenericType.INTEGER), + true); + } + + @Test + public void should_create_map_codec_for_java_value() { + MapType cqlType = DataTypes.mapOf(DataTypes.INT, DataTypes.mapOf(DataTypes.INT, DataTypes.INT)); + GenericType>> javaType = + new GenericType>>() {}; + Map> value = ImmutableMap.of(1, ImmutableMap.of(1, 1)); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec>> codec = registry.codecFor(value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(null, javaType, true); + inOrder + .verify(mockCache) + .lookup(null, GenericType.mapOf(GenericType.INTEGER, GenericType.INTEGER), true); + } + + @Test + public void should_create_map_codec_for_java_value_when_first_element_is_a_subtype() + throws UnknownHostException { + MapType cqlType = DataTypes.mapOf(DataTypes.INET, DataTypes.INET); + GenericType> javaType = + new GenericType>() {}; + InetAddress address = InetAddress.getByAddress(new byte[] {127, 0, 0, 1}); + // Because the actual implementation is a subclass, there is no exact match with the codec's + // declared type + assertThat(address).isInstanceOf(Inet4Address.class); + Map value = ImmutableMap.of(address, address); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec> codec = registry.codecFor(value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(javaType)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + + inOrder + .verify(mockCache) + .lookup(null, GenericType.mapOf(Inet4Address.class, Inet4Address.class), true); + } + + @Test + public void should_create_tuple_codec_for_cql_and_java_types() { + TupleType cqlType = DataTypes.tupleOf(DataTypes.INT, DataTypes.listOf(DataTypes.TEXT)); + TupleValue value = cqlType.newValue(); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec codec = registry.codecFor(cqlType, GenericType.TUPLE_VALUE); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(GenericType.TUPLE_VALUE)).isTrue(); + assertThat(codec.accepts(TupleValue.class)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, GenericType.TUPLE_VALUE, false); + // field codecs are only looked up when fields are accessed, so no cache hit for list now + + } + + @Test + public void should_create_tuple_codec_for_cql_type() { + TupleType cqlType = DataTypes.tupleOf(DataTypes.INT, DataTypes.listOf(DataTypes.TEXT)); + TupleValue value = cqlType.newValue(); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec codec = registry.codecFor(cqlType); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(GenericType.TUPLE_VALUE)).isTrue(); + assertThat(codec.accepts(TupleValue.class)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, null, false); + } + + @Test + public void should_create_tuple_codec_for_cql_type_and_java_value() { + TupleType cqlType = DataTypes.tupleOf(DataTypes.INT, DataTypes.listOf(DataTypes.TEXT)); + TupleValue value = cqlType.newValue(); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec codec = registry.codecFor(cqlType, value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(GenericType.TUPLE_VALUE)).isTrue(); + assertThat(codec.accepts(TupleValue.class)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, GenericType.TUPLE_VALUE, false); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void should_create_tuple_codec_for_java_value() { + TupleType cqlType = DataTypes.tupleOf(DataTypes.INT, DataTypes.listOf(DataTypes.TEXT)); + TupleValue value = cqlType.newValue(); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec codec = registry.codecFor(value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(GenericType.TUPLE_VALUE)).isTrue(); + assertThat(codec.accepts(TupleValue.class)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + // UDTs know their CQL type, so the actual lookup is by CQL + Java type, and therefore not + // covariant. + inOrder.verify(mockCache).lookup(cqlType, GenericType.TUPLE_VALUE, false); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void should_create_udt_codec_for_cql_and_java_types() { + UserDefinedType cqlType = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("field1"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("field2"), DataTypes.listOf(DataTypes.TEXT)) + .build(); + UdtValue value = cqlType.newValue(); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec codec = registry.codecFor(cqlType, GenericType.UDT_VALUE); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(GenericType.UDT_VALUE)).isTrue(); + assertThat(codec.accepts(UdtValue.class)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, GenericType.UDT_VALUE, false); + // field codecs are only looked up when fields are accessed, so no cache hit for list now + + } + + @Test + public void should_create_udt_codec_for_cql_type() { + UserDefinedType cqlType = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("field1"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("field2"), DataTypes.listOf(DataTypes.TEXT)) + .build(); + UdtValue value = cqlType.newValue(); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec codec = registry.codecFor(cqlType); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(GenericType.UDT_VALUE)).isTrue(); + assertThat(codec.accepts(UdtValue.class)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, null, false); + } + + @Test + public void should_create_udt_codec_for_cql_type_and_java_value() { + UserDefinedType cqlType = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("field1"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("field2"), DataTypes.listOf(DataTypes.TEXT)) + .build(); + UdtValue value = cqlType.newValue(); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec codec = registry.codecFor(cqlType, value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(GenericType.UDT_VALUE)).isTrue(); + assertThat(codec.accepts(UdtValue.class)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + inOrder.verify(mockCache).lookup(cqlType, GenericType.UDT_VALUE, false); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void should_create_udt_codec_for_java_value() { + UserDefinedType cqlType = + new UserDefinedTypeBuilder( + CqlIdentifier.fromInternal("ks"), CqlIdentifier.fromInternal("type")) + .withField(CqlIdentifier.fromInternal("field1"), DataTypes.INT) + .withField(CqlIdentifier.fromInternal("field2"), DataTypes.listOf(DataTypes.TEXT)) + .build(); + UdtValue value = cqlType.newValue(); + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache); + InOrder inOrder = inOrder(mockCache); + + TypeCodec codec = registry.codecFor(value); + assertThat(codec).isNotNull(); + assertThat(codec.accepts(cqlType)).isTrue(); + assertThat(codec.accepts(GenericType.UDT_VALUE)).isTrue(); + assertThat(codec.accepts(UdtValue.class)).isTrue(); + assertThat(codec.accepts(value)).isTrue(); + // UDTs know their CQL type, so the actual lookup is by CQL + Java type, and therefore not + // covariant. + inOrder.verify(mockCache).lookup(cqlType, GenericType.UDT_VALUE, false); + + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void should_not_find_codec_if_java_type_unknown() { + try { + CodecRegistry.DEFAULT.codecFor(StringBuilder.class); + fail("Should not have found a codec for ANY <-> StringBuilder"); + } catch (CodecNotFoundException e) { + // expected + } + try { + CodecRegistry.DEFAULT.codecFor(DataTypes.TEXT, StringBuilder.class); + fail("Should not have found a codec for varchar <-> StringBuilder"); + } catch (CodecNotFoundException e) { + // expected + } + try { + CodecRegistry.DEFAULT.codecFor(new StringBuilder()); + fail("Should not have found a codec for ANY <-> StringBuilder"); + } catch (CodecNotFoundException e) { + // expected + } + } + + @Test + public void should_not_allow_covariance_for_lookups_by_java_type() { + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache, new ACodec()); + InOrder inOrder = inOrder(mockCache); + + // covariance not allowed + + assertThatThrownBy(() -> registry.codecFor(B.class)) + .isInstanceOf(CodecNotFoundException.class) + .hasMessage("Codec not found for requested operation: [null <-> %s]", B.class.getName()); + // because of invariance, the custom A codec doesn't match so we try the cache + inOrder.verify(mockCache).lookup(null, GenericType.of(B.class), false); + inOrder.verifyNoMoreInteractions(); + + assertThatThrownBy(() -> registry.codecFor(GenericType.listOf(B.class))) + .isInstanceOf(CodecNotFoundException.class); + inOrder.verify(mockCache).lookup(null, GenericType.listOf(B.class), false); + inOrder.verify(mockCache).lookup(null, GenericType.of(B.class), false); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void should_allow_covariance_for_lookups_by_cql_type_and_value() { + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache, new ACodec()); + InOrder inOrder = inOrder(mockCache); + + // covariance allowed + + assertThat(registry.codecFor(DataTypes.INT, new B())).isInstanceOf(ACodec.class); + // no cache hit since we find the custom codec directly + inOrder.verifyNoMoreInteractions(); + + // note: in Java, type parameters are always invariant, so List is not a subtype of List; + // but in practice, a codec for List is capable of encoding a List, so we allow it (even + // if in driver 3.x that was forbidden). + List list = Lists.newArrayList(new B()); + ListType cqlType = DataTypes.listOf(DataTypes.INT); + TypeCodec> actual = registry.codecFor(cqlType, list); + assertThat(actual).isInstanceOf(ListCodec.class); + assertThat(actual.getJavaType()).isEqualTo(GenericType.listOf(A.class)); + assertThat(actual.accepts(list)).isTrue(); + // accepts(GenericType) remains invariant, so it returns false for List + assertThat(actual.accepts(GenericType.listOf(B.class))).isFalse(); + inOrder.verify(mockCache).lookup(cqlType, GenericType.listOf(B.class), true); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void should_allow_covariance_for_lookups_by_value() { + + TestCachingCodecRegistry registry = new TestCachingCodecRegistry(mockCache, new ACodec()); + InOrder inOrder = inOrder(mockCache); + + // covariance allowed + + assertThat(registry.codecFor(new B())).isInstanceOf(ACodec.class); + // no cache hit since we find the custom codec directly + inOrder.verifyNoMoreInteractions(); + + // note: in Java, type parameters are always invariant, so List is not a subtype of List; + // but in practice, a codec for List is capable of encoding a List, so we allow it (even + // if in driver 3.x that was forbidden). + List list = Lists.newArrayList(new B()); + TypeCodec> actual = registry.codecFor(list); + assertThat(actual).isInstanceOf(ListCodec.class); + assertThat(actual.getJavaType()).isEqualTo(GenericType.listOf(A.class)); + assertThat(actual.accepts(list)).isTrue(); + // accepts(GenericType) remains invariant, so it returns false for List + assertThat(actual.accepts(GenericType.listOf(B.class))).isFalse(); + inOrder.verify(mockCache).lookup(null, GenericType.listOf(B.class), true); + inOrder.verifyNoMoreInteractions(); + } + + // Our intent is not to test Guava cache, so we don't need an actual cache here. + // The only thing we want to check in our tests is if getCachedCodec was called. + public static class TestCachingCodecRegistry extends CachingCodecRegistry { + private final MockCache cache; + + public TestCachingCodecRegistry(MockCache cache, TypeCodec... userCodecs) { + super("test", CodecRegistryConstants.PRIMITIVE_CODECS, userCodecs); + this.cache = cache; + } + + @Override + protected TypeCodec getCachedCodec( + DataType cqlType, GenericType javaType, boolean isJavaCovariant) { + cache.lookup(cqlType, javaType, isJavaCovariant); + return createCodec(cqlType, javaType, isJavaCovariant); + } + + public interface MockCache { + void lookup(DataType cqlType, GenericType javaType, boolean isJavaCovariant); + } + } + + public static class TextToPeriodCodec implements TypeCodec { + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.of(Period.class); + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.TEXT; + } + + @Override + public ByteBuffer encode(Period value, @NonNull ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException("not implemented for this test"); + } + + @Override + public Period decode(ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException("not implemented for this test"); + } + + @NonNull + @Override + public String format(Period value) { + throw new UnsupportedOperationException("not implemented for this test"); + } + + @Override + public Period parse(String value) { + throw new UnsupportedOperationException("not implemented for this test"); + } + } + + private static class A {} + + private static class B extends A {} + + private static class ACodec implements TypeCodec { + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.of(A.class); + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.INT; + } + + @Override + public ByteBuffer encode(A value, @NonNull ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException("irrelevant"); + } + + @Override + public A decode(ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException("irrelevant"); + } + + @NonNull + @Override + public String format(A value) { + throw new UnsupportedOperationException("irrelevant"); + } + + @Override + public A parse(String value) { + throw new UnsupportedOperationException("irrelevant"); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/ArrayUtilsTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/ArrayUtilsTest.java new file mode 100644 index 00000000000..e6a878f7450 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/ArrayUtilsTest.java @@ -0,0 +1,141 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.concurrent.ThreadLocalRandom; +import org.junit.Test; + +public class ArrayUtilsTest { + + @Test + public void should_swap() { + String[] array = {"a", "b", "c"}; + ArrayUtils.swap(array, 0, 2); + assertThat(array).containsExactly("c", "b", "a"); + } + + @Test + public void should_swap_with_same_index() { + String[] array = {"a", "b", "c"}; + ArrayUtils.swap(array, 0, 0); + assertThat(array).containsExactly("a", "b", "c"); + } + + @Test + public void should_bubble_up() { + String[] array = {"a", "b", "c", "d", "e"}; + ArrayUtils.bubbleUp(array, 3, 1); + assertThat(array).containsExactly("a", "d", "b", "c", "e"); + } + + @Test + public void should_bubble_up_to_same_index() { + String[] array = {"a", "b", "c", "d", "e"}; + ArrayUtils.bubbleUp(array, 3, 3); + assertThat(array).containsExactly("a", "b", "c", "d", "e"); + } + + @Test + public void should_not_bubble_up_when_target_index_higher() { + String[] array = {"a", "b", "c", "d", "e"}; + ArrayUtils.bubbleUp(array, 3, 5); + assertThat(array).containsExactly("a", "b", "c", "d", "e"); + } + + @Test + public void should_bubble_down() { + String[] array = {"a", "b", "c", "d", "e"}; + ArrayUtils.bubbleDown(array, 1, 3); + assertThat(array).containsExactly("a", "c", "d", "b", "e"); + } + + @Test + public void should_bubble_down_to_same_index() { + String[] array = {"a", "b", "c", "d", "e"}; + ArrayUtils.bubbleDown(array, 3, 3); + assertThat(array).containsExactly("a", "b", "c", "d", "e"); + } + + @Test + public void should_not_bubble_down_when_target_index_lower() { + String[] array = {"a", "b", "c", "d", "e"}; + ArrayUtils.bubbleDown(array, 4, 2); + assertThat(array).containsExactly("a", "b", "c", "d", "e"); + } + + @Test + public void should_shuffle_head() { + String[] array = {"a", "b", "c", "d", "e"}; + ThreadLocalRandom random = mock(ThreadLocalRandom.class); + when(random.nextInt(anyInt())) + .thenAnswer( + (invocation) -> { + int i = invocation.getArgument(0); + // shifts elements by 1 to the right + return i - 2; + }); + ArrayUtils.shuffleHead(array, 3, random); + assertThat(array[0]).isEqualTo("c"); + assertThat(array[1]).isEqualTo("a"); + assertThat(array[2]).isEqualTo("b"); + // Tail elements should not move + assertThat(array[3]).isEqualTo("d"); + assertThat(array[4]).isEqualTo("e"); + } + + @Test(expected = ArrayIndexOutOfBoundsException.class) + public void should_fail_to_shuffle_head_when_count_is_too_high() { + ArrayUtils.shuffleHead(new String[] {"a", "b", "c"}, 5); + } + + @Test + public void should_rotate() { + String[] array = {"a", "b", "c", "d", "e"}; + + ArrayUtils.rotate(array, 1, 3, 1); + assertThat(array).containsExactly("a", "c", "d", "b", "e"); + + ArrayUtils.rotate(array, 0, 4, 2); + assertThat(array).containsExactly("d", "b", "a", "c", "e"); + + ArrayUtils.rotate(array, 2, 3, 10); + assertThat(array).containsExactly("d", "b", "c", "e", "a"); + } + + @Test + public void should_not_rotate_when_amount_multiple_of_range_size() { + String[] array = {"a", "b", "c", "d", "e"}; + + ArrayUtils.rotate(array, 1, 3, 9); + assertThat(array).containsExactly("a", "b", "c", "d", "e"); + } + + @Test + public void should_not_rotate_when_range_is_singleton_or_empty() { + String[] array = {"a", "b", "c", "d", "e"}; + + ArrayUtils.rotate(array, 1, 1, 3); + assertThat(array).containsExactly("a", "b", "c", "d", "e"); + + ArrayUtils.rotate(array, 1, 0, 3); + assertThat(array).containsExactly("a", "b", "c", "d", "e"); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/ByteBufs.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/ByteBufs.java new file mode 100644 index 00000000000..0983c1b8900 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/ByteBufs.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import com.datastax.oss.protocol.internal.util.Bytes; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.nio.ByteBuffer; + +/** Helper class to create {@link io.netty.buffer.ByteBuf} instances in tests. */ +public class ByteBufs { + public static ByteBuf wrap(int... bytes) { + ByteBuf bb = ByteBufAllocator.DEFAULT.buffer(bytes.length); + for (int b : bytes) { + bb.writeByte(b); + } + return bb; + } + + public static ByteBuf fromHexString(String hexString) { + ByteBuffer tmp = Bytes.fromHexString(hexString); + ByteBuf target = ByteBufAllocator.DEFAULT.buffer(tmp.remaining()); + target.writeBytes(tmp); + return target; + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/DirectedGraphTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/DirectedGraphTest.java new file mode 100644 index 00000000000..b673edcf1d2 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/DirectedGraphTest.java @@ -0,0 +1,81 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public class DirectedGraphTest { + + @Test + public void should_sort_empty_graph() { + DirectedGraph g = new DirectedGraph<>(); + assertThat(g.topologicalSort()).isEmpty(); + } + + @Test + public void should_sort_graph_with_one_node() { + DirectedGraph g = new DirectedGraph<>("A"); + assertThat(g.topologicalSort()).containsExactly("A"); + } + + @Test + public void should_sort_complex_graph() { + // H G + // / \ /\ + // F | E + // \ / / + // D / + // / \/ + // B C + // | + // A + DirectedGraph g = new DirectedGraph<>("A", "B", "C", "D", "E", "F", "G", "H"); + g.addEdge("H", "F"); + g.addEdge("G", "E"); + g.addEdge("H", "D"); + g.addEdge("F", "D"); + g.addEdge("G", "D"); + g.addEdge("D", "C"); + g.addEdge("E", "C"); + g.addEdge("D", "B"); + g.addEdge("B", "A"); + + // The graph uses linked hash maps internally, so this order will be consistent across JVMs + assertThat(g.topologicalSort()).containsExactly("G", "H", "E", "F", "D", "C", "B", "A"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_sort_if_graph_has_a_cycle() { + DirectedGraph g = new DirectedGraph<>("A", "B", "C"); + g.addEdge("A", "B"); + g.addEdge("B", "C"); + g.addEdge("C", "B"); + + g.topologicalSort(); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_to_sort_if_graph_is_a_cycle() { + DirectedGraph g = new DirectedGraph<>("A", "B", "C"); + g.addEdge("A", "B"); + g.addEdge("B", "C"); + g.addEdge("C", "A"); + + g.topologicalSort(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/ReflectionTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/ReflectionTest.java new file mode 100644 index 00000000000..a809e7b0c9b --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/ReflectionTest.java @@ -0,0 +1,82 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.specex.SpeculativeExecutionPolicy; +import com.datastax.oss.driver.internal.core.config.typesafe.TypesafeDriverConfig; +import com.datastax.oss.driver.internal.core.context.InternalDriverContext; +import com.datastax.oss.driver.internal.core.specex.ConstantSpeculativeExecutionPolicy; +import com.datastax.oss.driver.internal.core.specex.NoSpeculativeExecutionPolicy; +import com.typesafe.config.ConfigFactory; +import java.util.Map; +import org.junit.Test; + +public class ReflectionTest { + + @Test + public void should_build_policies_per_profile() { + String configSource = + "advanced.speculative-execution-policy {\n" + + " class = ConstantSpeculativeExecutionPolicy\n" + + " max-executions = 3\n" + + " delay = 100 milliseconds\n" + + "}\n" + + "profiles {\n" + // Inherits from default profile + + " profile1 {}\n" + // Inherits but changes one option + + " profile2 { \n" + + " advanced.speculative-execution-policy.max-executions = 2" + + " }\n" + // Same as previous profile, should share the same policy instance + + " profile3 { \n" + + " advanced.speculative-execution-policy.max-executions = 2" + + " }\n" + // Completely overrides default profile + + " profile4 { \n" + + " advanced.speculative-execution-policy.class = NoSpeculativeExecutionPolicy\n" + + " }\n" + + "}\n"; + InternalDriverContext context = mock(InternalDriverContext.class); + TypesafeDriverConfig config = new TypesafeDriverConfig(ConfigFactory.parseString(configSource)); + when(context.getConfig()).thenReturn(config); + + Map policies = + Reflection.buildFromConfigProfiles( + context, + DefaultDriverOption.SPECULATIVE_EXECUTION_POLICY, + SpeculativeExecutionPolicy.class, + "com.datastax.oss.driver.internal.core.specex"); + + assertThat(policies).hasSize(5); + SpeculativeExecutionPolicy defaultPolicy = policies.get(DriverExecutionProfile.DEFAULT_NAME); + SpeculativeExecutionPolicy policy1 = policies.get("profile1"); + SpeculativeExecutionPolicy policy2 = policies.get("profile2"); + SpeculativeExecutionPolicy policy3 = policies.get("profile3"); + SpeculativeExecutionPolicy policy4 = policies.get("profile4"); + assertThat(defaultPolicy) + .isInstanceOf(ConstantSpeculativeExecutionPolicy.class) + .isSameAs(policy1); + assertThat(policy2).isInstanceOf(ConstantSpeculativeExecutionPolicy.class).isSameAs(policy3); + assertThat(policy4).isInstanceOf(NoSpeculativeExecutionPolicy.class); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/collection/QueryPlanTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/collection/QueryPlanTest.java new file mode 100644 index 00000000000..8157a2662ee --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/collection/QueryPlanTest.java @@ -0,0 +1,77 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.collection; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.metadata.Node; +import java.util.Iterator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class QueryPlanTest { + + @Mock private Node node1; + @Mock private Node node2; + @Mock private Node node3; + + @Test + public void should_poll_elements() { + QueryPlan queryPlan = new QueryPlan(node1, node2, node3); + assertThat(queryPlan.poll()).isSameAs(node1); + assertThat(queryPlan.poll()).isSameAs(node2); + assertThat(queryPlan.poll()).isSameAs(node3); + assertThat(queryPlan.poll()).isNull(); + assertThat(queryPlan.poll()).isNull(); + } + + @Test + public void should_return_size() { + QueryPlan queryPlan = new QueryPlan(node1, node2, node3); + assertThat(queryPlan.size()).isEqualTo(3); + queryPlan.poll(); + assertThat(queryPlan.size()).isEqualTo(2); + queryPlan.poll(); + assertThat(queryPlan.size()).isEqualTo(1); + queryPlan.poll(); + assertThat(queryPlan.size()).isEqualTo(0); + queryPlan.poll(); + assertThat(queryPlan.size()).isEqualTo(0); + } + + @Test + public void should_return_iterator() { + QueryPlan queryPlan = new QueryPlan(node1, node2, node3); + Iterator iterator3 = queryPlan.iterator(); + queryPlan.poll(); + Iterator iterator2 = queryPlan.iterator(); + queryPlan.poll(); + Iterator iterator1 = queryPlan.iterator(); + queryPlan.poll(); + Iterator iterator0 = queryPlan.iterator(); + queryPlan.poll(); + Iterator iterator00 = queryPlan.iterator(); + + assertThat(iterator3).toIterable().containsExactly(node1, node2, node3); + assertThat(iterator2).toIterable().containsExactly(node2, node3); + assertThat(iterator1).toIterable().containsExactly(node3); + assertThat(iterator0).toIterable().isEmpty(); + assertThat(iterator00).toIterable().isEmpty(); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/CapturingTimer.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/CapturingTimer.java new file mode 100644 index 00000000000..3b2cda52b35 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/CapturingTimer.java @@ -0,0 +1,137 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.datastax.oss.driver.internal.core.util.concurrent; + +import static org.assertj.core.api.Assertions.fail; + +import io.netty.util.Timeout; +import io.netty.util.Timer; +import io.netty.util.TimerTask; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Implementation of Netty's {@link io.netty.util.Timer Timer} interface to capture scheduled {@link + * io.netty.util.Timeout Timeouts} instead of running them, so they can be run manually in tests. + */ +public class CapturingTimer implements Timer { + + private final ArrayBlockingQueue timeoutQueue = new ArrayBlockingQueue<>(16); + + @Override + public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) { + // delay and unit are not needed as the Timeout's TimerTask will be run manually + CapturedTimeout timeout = new CapturedTimeout(task, this, delay, unit); + // add the timeout to the queue + timeoutQueue.add(timeout); + return timeout; + } + + /** + * Retrieves the next scheduled Timeout. In tests, this will usually be a request timeout or a + * speculative execution. Tests will need be able to predict the ordering as it is not easy to + * tell from the returned Timeout itself. + */ + public CapturedTimeout getNextTimeout() { + try { + return timeoutQueue.poll(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException ie) { + fail("Unexpected interruption", ie); + throw new AssertionError(); + } + } + + @Override + public Set stop() { + if (timeoutQueue.isEmpty()) { + return Collections.emptySet(); + } + Set timeoutsRemaining = new HashSet<>(timeoutQueue.size()); + for (Timeout t : timeoutQueue) { + if (t != null) { + t.cancel(); + timeoutsRemaining.add(t); + } + } + return timeoutsRemaining; + } + + /** + * Implementation of Netty's {@link io.netty.util.Timeout Timeout} interface. It is just a simple + * class that keeps track of the {@link io.netty.util.TimerTask TimerTask} and the {@link + * io.netty.util.Timer Timer} implementation that should only be used in tests. The intended use + * is to call the {@link io.netty.util.TimerTask#run(io.netty.util.Timeout) run()} method on the + * TimerTask when you want to execute the task (so you don't have to depend on a real timer). + * + *

Example: + * + *

{@code
+   * // get the next timeout from the timer
+   * Timeout t = timer.getNextTimeout();
+   * // run the TimerTask associated with the timeout
+   * t.task.run(t);
+   * }
+ */ + public static class CapturedTimeout implements Timeout { + + private final TimerTask task; + private final CapturingTimer timer; + private final long delay; + private final TimeUnit unit; + private final AtomicBoolean cancelled = new AtomicBoolean(false); + + private CapturedTimeout(TimerTask task, CapturingTimer timer, long delay, TimeUnit unit) { + this.task = task; + this.timer = timer; + this.delay = delay; + this.unit = unit; + } + + @Override + public Timer timer() { + return timer; + } + + @Override + public TimerTask task() { + return task; + } + + public long getDelay(TimeUnit targetUnit) { + return targetUnit.convert(delay, unit); + } + + @Override + public boolean isExpired() { + return false; + } + + @Override + public boolean isCancelled() { + return cancelled.get(); + } + + @Override + public boolean cancel() { + return cancelled.compareAndSet(false, true); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/CycleDetectorTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/CycleDetectorTest.java new file mode 100644 index 00000000000..d8de58564f5 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/CycleDetectorTest.java @@ -0,0 +1,118 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.ThreadFactoryBuilder; +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.Uninterruptibles; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.junit.Test; + +public class CycleDetectorTest { + + @Test + public void should_detect_cycle_within_same_thread() { + CycleDetector checker = new CycleDetector("Detected cycle", true); + CyclicContext context = new CyclicContext(checker, false); + try { + context.a.get(); + fail("Expected an exception"); + } catch (Exception e) { + assertThat(e) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Detected cycle"); + } + } + + @Test + public void should_detect_cycle_between_different_threads() throws Throwable { + CycleDetector checker = new CycleDetector("Detected cycle", true); + CyclicContext context = new CyclicContext(checker, true); + ExecutorService executor = + Executors.newFixedThreadPool( + 3, new ThreadFactoryBuilder().setNameFormat("thread%d").build()); + Future futureA = executor.submit(() -> context.a.get()); + Future futureB = executor.submit(() -> context.b.get()); + Future futureC = executor.submit(() -> context.c.get()); + context.latchA.countDown(); + context.latchB.countDown(); + context.latchC.countDown(); + for (Future future : ImmutableList.of(futureA, futureB, futureC)) { + try { + Uninterruptibles.getUninterruptibly(future); + } catch (ExecutionException e) { + assertThat(e.getCause()) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Detected cycle"); + } + } + } + + private static class CyclicContext { + private LazyReference a; + private LazyReference b; + private LazyReference c; + private CountDownLatch latchA; + private CountDownLatch latchB; + private CountDownLatch latchC; + + private CyclicContext(CycleDetector checker, boolean enableLatches) { + this.a = new LazyReference<>("a", this::buildA, checker); + this.b = new LazyReference<>("b", this::buildB, checker); + this.c = new LazyReference<>("c", this::buildC, checker); + if (enableLatches) { + this.latchA = new CountDownLatch(1); + this.latchB = new CountDownLatch(1); + this.latchC = new CountDownLatch(1); + } + } + + private String buildA() { + maybeAwaitUninterruptibly(latchA); + b.get(); + return "a"; + } + + private String buildB() { + maybeAwaitUninterruptibly(latchB); + c.get(); + return "b"; + } + + private String buildC() { + maybeAwaitUninterruptibly(latchC); + a.get(); + return "c"; + } + + private static void maybeAwaitUninterruptibly(CountDownLatch latch) { + if (latch != null) { + try { + latch.await(); + } catch (InterruptedException e) { + fail("interrupted", e); + } + } + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/DebouncerTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/DebouncerTest.java new file mode 100644 index 00000000000..2cd6c9eed21 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/DebouncerTest.java @@ -0,0 +1,179 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import static com.datastax.oss.driver.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.shaded.guava.common.base.Joiner; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.ScheduledFuture; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class DebouncerTest { + + private static final Duration DEFAULT_WINDOW = Duration.ofSeconds(1); + private static final int DEFAULT_MAX_EVENTS = 10; + + @Mock private EventExecutor adminExecutor; + @Mock private ScheduledFuture scheduledFuture; + private List results; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(adminExecutor.inEventLoop()).thenReturn(true); + when(adminExecutor.schedule( + any(Runnable.class), eq(DEFAULT_WINDOW.toNanos()), eq(TimeUnit.NANOSECONDS))) + .thenAnswer((i) -> scheduledFuture); + results = new ArrayList<>(); + } + + private String coalesce(List events) { + return Joiner.on(",").join(events); + } + + private void flush(String result) { + results.add(result); + } + + @Test + public void should_flush_synchronously_if_window_is_zero() { + Debouncer debouncer = + new Debouncer<>( + adminExecutor, this::coalesce, this::flush, Duration.ZERO, DEFAULT_MAX_EVENTS); + + debouncer.receive(1); + debouncer.receive(2); + + verify(adminExecutor, never()).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class)); + + assertThat(results).containsExactly("1", "2"); + } + + @Test + public void should_flush_synchronously_if_max_events_is_one() { + Debouncer debouncer = + new Debouncer<>(adminExecutor, this::coalesce, this::flush, DEFAULT_WINDOW, 1); + + debouncer.receive(1); + debouncer.receive(2); + + verify(adminExecutor, never()).schedule(any(Runnable.class), anyLong(), any(TimeUnit.class)); + + assertThat(results).containsExactly("1", "2"); + } + + @Test + public void should_debounce_after_time_window_if_no_other_event() { + Debouncer debouncer = + new Debouncer<>( + adminExecutor, this::coalesce, this::flush, DEFAULT_WINDOW, DEFAULT_MAX_EVENTS); + debouncer.receive(1); + + // a task should have been scheduled, run it + ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + verify(adminExecutor) + .schedule(captor.capture(), eq(DEFAULT_WINDOW.toNanos()), eq(TimeUnit.NANOSECONDS)); + captor.getValue().run(); + + // the element should have been flushed + assertThat(results).containsExactly("1"); + } + + @Test + public void should_reset_time_window_when_new_event() { + Debouncer debouncer = + new Debouncer<>( + adminExecutor, this::coalesce, this::flush, DEFAULT_WINDOW, DEFAULT_MAX_EVENTS); + debouncer.receive(1); + debouncer.receive(2); + + InOrder inOrder = inOrder(adminExecutor, scheduledFuture); + + // a first task should have been scheduled, and then cancelled + inOrder + .verify(adminExecutor) + .schedule(any(Runnable.class), eq(DEFAULT_WINDOW.toNanos()), eq(TimeUnit.NANOSECONDS)); + inOrder.verify(scheduledFuture).cancel(true); + + // a second task should have been scheduled, run it + ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + inOrder + .verify(adminExecutor) + .schedule(captor.capture(), eq(DEFAULT_WINDOW.toNanos()), eq(TimeUnit.NANOSECONDS)); + captor.getValue().run(); + + // both elements should have been flushed together + assertThat(results).containsExactly("1,2"); + } + + @Test + public void should_force_flush_after_max_events() { + Debouncer debouncer = + new Debouncer<>( + adminExecutor, this::coalesce, this::flush, DEFAULT_WINDOW, DEFAULT_MAX_EVENTS); + for (int i = 0; i < 10; i++) { + debouncer.receive(i); + } + verify(adminExecutor, times(9)) + .schedule(any(Runnable.class), eq(DEFAULT_WINDOW.toNanos()), eq(TimeUnit.NANOSECONDS)); + verify(scheduledFuture, times(9)).cancel(true); + assertThat(results).containsExactly("0,1,2,3,4,5,6,7,8,9"); + } + + @Test + public void should_cancel_next_flush_when_stopped() { + Debouncer debouncer = + new Debouncer<>( + adminExecutor, this::coalesce, this::flush, DEFAULT_WINDOW, DEFAULT_MAX_EVENTS); + + debouncer.receive(1); + verify(adminExecutor) + .schedule(any(Runnable.class), eq(DEFAULT_WINDOW.toNanos()), eq(TimeUnit.NANOSECONDS)); + + debouncer.stop(); + verify(scheduledFuture).cancel(true); + } + + @Test + public void should_ignore_new_events_when_flushed() { + Debouncer debouncer = + new Debouncer<>( + adminExecutor, this::coalesce, this::flush, DEFAULT_WINDOW, DEFAULT_MAX_EVENTS); + debouncer.stop(); + + debouncer.receive(1); + verify(adminExecutor, never()) + .schedule(any(Runnable.class), eq(DEFAULT_WINDOW.toNanos()), eq(TimeUnit.NANOSECONDS)); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ReconnectionTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ReconnectionTest.java new file mode 100644 index 00000000000..24bd02b1b73 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ReconnectionTest.java @@ -0,0 +1,359 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.TestDataProviders; +import com.datastax.oss.driver.api.core.connection.ReconnectionPolicy.ReconnectionSchedule; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.concurrent.EventExecutor; +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(DataProviderRunner.class) +public class ReconnectionTest { + + @Mock private ReconnectionSchedule reconnectionSchedule; + @Mock private Runnable onStartCallback; + @Mock private Runnable onStopCallback; + private EmbeddedChannel channel; + + private MockReconnectionTask reconnectionTask; + private Reconnection reconnection; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + // Unfortunately Netty does not expose EmbeddedEventLoop, so we have to go through a channel + channel = new EmbeddedChannel(); + EventExecutor eventExecutor = channel.eventLoop(); + + reconnectionTask = new MockReconnectionTask(); + reconnection = + new Reconnection( + "test", + eventExecutor, + () -> reconnectionSchedule, + reconnectionTask, + onStartCallback, + onStopCallback); + } + + @Test + public void should_start_out_not_running() { + assertThat(reconnection.isRunning()).isFalse(); + } + + @Test + public void should_schedule_first_attempt_on_start() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofSeconds(1)); + + // When + reconnection.start(); + + // Then + verify(reconnectionSchedule).nextDelay(); + assertThat(reconnection.isRunning()).isTrue(); + verify(onStartCallback).run(); + } + + @Test + public void should_ignore_start_if_already_started() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofSeconds(1)); + reconnection.start(); + verify(reconnectionSchedule).nextDelay(); + verify(onStartCallback).run(); + + // When + reconnection.start(); + + // Then + verifyNoMoreInteractions(reconnectionSchedule, onStartCallback); + } + + @Test + public void should_stop_if_first_attempt_succeeds() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + reconnection.start(); + verify(reconnectionSchedule).nextDelay(); + + // When + // the reconnection task is scheduled: + runPendingTasks(); + assertThat(reconnectionTask.callCount()).isEqualTo(1); + // the reconnection task completes: + reconnectionTask.complete(true); + runPendingTasks(); + + // Then + assertThat(reconnection.isRunning()).isFalse(); + verify(onStopCallback).run(); + } + + @Test + public void should_reschedule_if_first_attempt_fails() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + reconnection.start(); + verify(reconnectionSchedule).nextDelay(); + + // When + // the reconnection task is scheduled: + runPendingTasks(); + assertThat(reconnectionTask.callCount()).isEqualTo(1); + // the reconnection task completes: + reconnectionTask.complete(false); + runPendingTasks(); + + // Then + // schedule was called again + verify(reconnectionSchedule, times(2)).nextDelay(); + runPendingTasks(); + // task was called again + assertThat(reconnectionTask.callCount()).isEqualTo(2); + // still running + assertThat(reconnection.isRunning()).isTrue(); + + // When + // second attempt completes + reconnectionTask.complete(true); + runPendingTasks(); + + // Then + assertThat(reconnection.isRunning()).isFalse(); + verify(onStopCallback).run(); + } + + @Test + public void should_reconnect_now_if_next_attempt_not_started() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofDays(1)); + reconnection.start(); + verify(reconnectionSchedule).nextDelay(); + + // When + reconnection.reconnectNow(false); + runPendingTasks(); + + // Then + // reconnection task was run immediately + assertThat(reconnectionTask.callCount()).isEqualTo(1); + // if that attempt fails, another reconnection should be scheduled + reconnectionTask.complete(false); + runPendingTasks(); + verify(reconnectionSchedule, times(2)).nextDelay(); + } + + @Test + public void should_reconnect_now_if_stopped_and_forced() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofDays(1)); + assertThat(reconnection.isRunning()).isFalse(); + + // When + reconnection.reconnectNow(true); + runPendingTasks(); + + // Then + // reconnection task was run immediately + assertThat(reconnectionTask.callCount()).isEqualTo(1); + // if that attempt failed, another reconnection was scheduled + reconnectionTask.complete(false); + runPendingTasks(); + verify(reconnectionSchedule).nextDelay(); + } + + @Test + @UseDataProvider(location = TestDataProviders.class, value = "booleans") + public void should_reconnect_now_when_attempt_in_progress(boolean force) { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + reconnection.start(); + runPendingTasks(); + // the next scheduled attempt has started, but not completed yet + assertThat(reconnectionTask.callCount()).isEqualTo(1); + + // When + reconnection.reconnectNow(force); + runPendingTasks(); + + // Then + // reconnection task should not have been called again + assertThat(reconnectionTask.callCount()).isEqualTo(1); + // should still run until current attempt completes + assertThat(reconnection.isRunning()).isTrue(); + reconnectionTask.complete(true); + runPendingTasks(); + assertThat(reconnection.isRunning()).isFalse(); + } + + @Test + public void should_not_reconnect_now_if_stopped_and_not_forced() { + // Given + assertThat(reconnection.isRunning()).isFalse(); + + // When + reconnection.reconnectNow(false); + runPendingTasks(); + + // Then + assertThat(reconnectionTask.callCount()).isEqualTo(0); + } + + @Test + public void should_stop_between_attempts() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofSeconds(10)); + reconnection.start(); + runPendingTasks(); + verify(reconnectionSchedule).nextDelay(); + + // When + reconnection.stop(); + runPendingTasks(); + + // Then + verify(onStopCallback).run(); + assertThat(reconnection.isRunning()).isFalse(); + } + + @Test + public void should_restart_after_stopped_between_attempts() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofSeconds(10)); + reconnection.start(); + runPendingTasks(); + verify(reconnectionSchedule).nextDelay(); + reconnection.stop(); + runPendingTasks(); + assertThat(reconnection.isRunning()).isFalse(); + + // When + reconnection.start(); + runPendingTasks(); + + // Then + verify(reconnectionSchedule, times(2)).nextDelay(); + assertThat(reconnection.isRunning()).isTrue(); + } + + @Test + @UseDataProvider(location = TestDataProviders.class, value = "booleans") + public void should_stop_while_attempt_in_progress(boolean outcome) { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + reconnection.start(); + runPendingTasks(); + // the next scheduled attempt has started, but not completed yet + assertThat(reconnectionTask.callCount()).isEqualTo(1); + verify(onStartCallback).run(); + + // When + reconnection.stop(); + runPendingTasks(); + + // Then + // should let the current attempt complete (whatever its outcome), and become stopped only then + assertThat(reconnection.isRunning()).isTrue(); + verifyNoMoreInteractions(onStopCallback); + reconnectionTask.complete(outcome); + runPendingTasks(); + verify(onStopCallback).run(); + assertThat(reconnection.isRunning()).isFalse(); + } + + @Test + public void should_restart_after_stopped_while_attempt_in_progress() { + // Given + when(reconnectionSchedule.nextDelay()).thenReturn(Duration.ofNanos(1)); + reconnection.start(); + runPendingTasks(); + // the next scheduled attempt has started, but not completed yet + assertThat(reconnectionTask.callCount()).isEqualTo(1); + verify(onStartCallback).run(); + // now stop + reconnection.stop(); + runPendingTasks(); + assertThat(reconnection.isRunning()).isTrue(); + + // When + reconnection.start(); + runPendingTasks(); + + // Then + assertThat(reconnection.isRunning()).isTrue(); + // still waiting on the same attempt, should not have called the task again + assertThat(reconnectionTask.callCount()).isEqualTo(1); + // because we were still in progress all the time, to the outside it's as if the stop/restart + // had never happened + verifyNoMoreInteractions(onStartCallback); + verifyNoMoreInteractions(onStopCallback); + + // When + reconnectionTask.complete(true); + runPendingTasks(); + + // Then + assertThat(reconnection.isRunning()).isFalse(); + verify(onStopCallback).run(); + } + + private void runPendingTasks() { + channel.runPendingTasks(); + } + + private static class MockReconnectionTask implements Callable> { + private volatile CompletableFuture nextResult; + private final AtomicInteger callCount = new AtomicInteger(); + + @Override + public CompletionStage call() throws Exception { + assertThat(nextResult == null || nextResult.isDone()).isTrue(); + callCount.incrementAndGet(); + nextResult = new CompletableFuture<>(); + return nextResult; + } + + private void complete(boolean outcome) { + assertThat(nextResult != null || !nextResult.isDone()).isTrue(); + nextResult.complete(outcome); + nextResult = null; + } + + private int callCount() { + return callCount.get(); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ReplayingEventFilterTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ReplayingEventFilterTest.java new file mode 100644 index 00000000000..6dcda9119f1 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ReplayingEventFilterTest.java @@ -0,0 +1,65 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; + +public class ReplayingEventFilterTest { + private ReplayingEventFilter filter; + private List filteredEvents; + + @Before + public void setup() { + filteredEvents = new ArrayList<>(); + filter = new ReplayingEventFilter<>(filteredEvents::add); + } + + @Test + public void should_discard_events_until_started() { + filter.accept(1); + filter.accept(2); + assertThat(filteredEvents).isEmpty(); + } + + @Test + public void should_accumulate_events_when_started() { + filter.accept(1); + filter.accept(2); + filter.start(); + filter.accept(3); + filter.accept(4); + assertThat(filter.recordedEvents()).containsExactly(3, 4); + } + + @Test + public void should_flush_accumulated_events_when_ready() { + filter.accept(1); + filter.accept(2); + filter.start(); + filter.accept(3); + filter.accept(4); + filter.markReady(); + assertThat(filteredEvents).containsExactly(3, 4); + filter.accept(5); + filter.accept(6); + assertThat(filteredEvents).containsExactly(3, 4, 5, 6); + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ScheduledTaskCapturingEventLoop.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ScheduledTaskCapturingEventLoop.java new file mode 100644 index 00000000000..79f56fb3215 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ScheduledTaskCapturingEventLoop.java @@ -0,0 +1,182 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.datastax.oss.driver.shaded.guava.common.util.concurrent.Uninterruptibles; +import edu.umd.cs.findbugs.annotations.NonNull; +import io.netty.channel.DefaultEventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.util.concurrent.ScheduledFuture; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Extend Netty's default event loop to capture scheduled tasks instead of running them. The tasks + * can be checked later, and run manually. + * + *

Tasks submitted with {@link #execute(Runnable)} or {@link #submit(Callable)} are still + * executed normally. + * + *

This is used to make unit tests independent of time. + */ +@SuppressWarnings("FunctionalInterfaceClash") // does not matter for test code +public class ScheduledTaskCapturingEventLoop extends DefaultEventLoop { + + private final BlockingQueue capturedTasks = new ArrayBlockingQueue<>(100); + + public ScheduledTaskCapturingEventLoop(EventLoopGroup parent) { + super(parent); + } + + @NonNull + @Override + public ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) { + CapturedTask task = new CapturedTask<>(callable, delay, unit); + boolean added = capturedTasks.offer(task); + assertThat(added).isTrue(); + return task.scheduledFuture; + } + + @NonNull + @Override + public ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + return schedule( + () -> { + command.run(); + return null; + }, + delay, + unit); + } + + @NonNull + @Override + public ScheduledFuture scheduleAtFixedRate( + Runnable command, long initialDelay, long period, TimeUnit unit) { + CapturedTask task = + new CapturedTask<>( + () -> { + command.run(); + return null; + }, + initialDelay, + period, + unit); + boolean added = capturedTasks.offer(task); + assertThat(added).isTrue(); + return task.scheduledFuture; + } + + @NonNull + @Override + public ScheduledFuture scheduleWithFixedDelay( + Runnable command, long initialDelay, long delay, TimeUnit unit) { + throw new UnsupportedOperationException("Not supported yet"); + } + + public CapturedTask nextTask() { + try { + return capturedTasks.poll(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + fail("Unexpected interruption", e); + throw new AssertionError(); + } + } + + /** + * Wait for any pending non-scheduled task (submitted with {@code submit}, {@code execute}, etc.) + * to complete. + */ + public void waitForNonScheduledTasks() { + ScheduledFuture f = super.schedule(() -> null, 5, TimeUnit.NANOSECONDS); + try { + Uninterruptibles.getUninterruptibly(f, 1, TimeUnit.SECONDS); + } catch (ExecutionException e) { + fail("unexpected error", e.getCause()); + } catch (TimeoutException e) { + fail("timed out while waiting for admin tasks to complete", e); + } + } + + public class CapturedTask { + private final FutureTask futureTask; + private final long initialDelay; + private final long period; + private final TimeUnit unit; + + @SuppressWarnings("unchecked") + private final ScheduledFuture scheduledFuture = mock(ScheduledFuture.class); + + CapturedTask(Callable task, long initialDelay, TimeUnit unit) { + this(task, initialDelay, -1, unit); + } + + CapturedTask(Callable task, long initialDelay, long period, TimeUnit unit) { + this.futureTask = new FutureTask<>(task); + this.initialDelay = initialDelay; + this.period = period; + this.unit = unit; + + // If the code under test cancels the scheduled future, cancel our task + when(scheduledFuture.cancel(anyBoolean())) + .thenAnswer(invocation -> futureTask.cancel(invocation.getArgument(0))); + + // Delegate methods of the scheduled future to our task (to be extended to more methods if + // needed) + when(scheduledFuture.isDone()).thenAnswer(invocation -> futureTask.isDone()); + when(scheduledFuture.isCancelled()).thenAnswer(invocation -> futureTask.isCancelled()); + } + + public void run() { + submit(futureTask); + waitForNonScheduledTasks(); + } + + public boolean isCancelled() { + // futureTask.isCancelled() can create timing issues in CI environments, so give the + // cancellation a short time to complete instead: + try { + futureTask.get(3, TimeUnit.SECONDS); + } catch (CancellationException e) { + return true; + } catch (Exception e) { + // ignore + } + return false; + } + + public long getInitialDelay(TimeUnit targetUnit) { + return targetUnit.convert(initialDelay, unit); + } + + /** By convention, non-recurring tasks have a negative period */ + public long getPeriod(TimeUnit targetUnit) { + return targetUnit.convert(period, unit); + } + } +} diff --git a/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ScheduledTaskCapturingEventLoopTest.java b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ScheduledTaskCapturingEventLoopTest.java new file mode 100644 index 00000000000..61bc52c99e1 --- /dev/null +++ b/core/src/test/java/com/datastax/oss/driver/internal/core/util/concurrent/ScheduledTaskCapturingEventLoopTest.java @@ -0,0 +1,64 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.core.util.concurrent; + +import static com.datastax.oss.driver.Assertions.assertThat; + +import com.datastax.oss.driver.internal.core.util.concurrent.ScheduledTaskCapturingEventLoop.CapturedTask; +import io.netty.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Test; + +public class ScheduledTaskCapturingEventLoopTest { + + @Test + public void should_capture_task_and_let_test_complete_it_manually() { + ScheduledTaskCapturingEventLoop eventLoop = new ScheduledTaskCapturingEventLoop(null); + final AtomicBoolean ran = new AtomicBoolean(); + ScheduledFuture future = eventLoop.schedule(() -> ran.set(true), 1, TimeUnit.NANOSECONDS); + + assertThat(future.isDone()).isFalse(); + assertThat(future.isCancelled()).isFalse(); + assertThat(ran.get()).isFalse(); + + CapturedTask task = eventLoop.nextTask(); + assertThat(task.getInitialDelay(TimeUnit.NANOSECONDS)).isEqualTo(1); + + task.run(); + + assertThat(future.isDone()).isTrue(); + assertThat(future.isCancelled()).isFalse(); + assertThat(ran.get()).isTrue(); + } + + @Test + public void should_let_tested_code_cancel_future() { + ScheduledTaskCapturingEventLoop eventLoop = new ScheduledTaskCapturingEventLoop(null); + final AtomicBoolean ran = new AtomicBoolean(); + ScheduledFuture future = eventLoop.schedule(() -> ran.set(true), 1, TimeUnit.NANOSECONDS); + + assertThat(future.isDone()).isFalse(); + assertThat(future.isCancelled()).isFalse(); + assertThat(ran.get()).isFalse(); + + future.cancel(true); + + assertThat(future.isDone()).isTrue(); + assertThat(future.isCancelled()).isTrue(); + assertThat(ran.get()).isFalse(); + } +} diff --git a/core/src/test/resources/logback-test.xml b/core/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..620eccb1c0c --- /dev/null +++ b/core/src/test/resources/logback-test.xml @@ -0,0 +1,29 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/distribution/pom.xml b/distribution/pom.xml new file mode 100644 index 00000000000..45f39aad8aa --- /dev/null +++ b/distribution/pom.xml @@ -0,0 +1,149 @@ + + + 4.0.0 + + + com.datastax.oss + java-driver-parent + 4.0.0 + + + java-driver-distribution + + jar + + DataStax Java driver for Apache Cassandra(R) - binary distribution + + + + + ${project.groupId} + java-driver-core + ${project.version} + + + ${project.groupId} + java-driver-query-builder + ${project.version} + + + + + datastax-java-driver-${project.version} + + + maven-jar-plugin + + + + default-jar + none + + + + + maven-source-plugin + + true + + + + maven-install-plugin + + true + + + + maven-deploy-plugin + + true + + + + org.revapi + revapi-maven-plugin + + true + + + + + + + + release + + + + maven-javadoc-plugin + + + dependencies-javadoc + + process-classes + + jar + + + true + DataStax Java driver for Apache Cassandra® ${project.version} API + + DataStax Java driver for Apache Cassandra(R) ${project.version} API + + + + + + + maven-assembly-plugin + + + assemble-binary-tarball + package + + single + + + + + false + + src/assembly/binary-tarball.xml + + posix + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + true + + + + + + + \ No newline at end of file diff --git a/distribution/src/assembly/binary-tarball.xml b/distribution/src/assembly/binary-tarball.xml new file mode 100644 index 00000000000..982b38a9850 --- /dev/null +++ b/distribution/src/assembly/binary-tarball.xml @@ -0,0 +1,133 @@ + + + binary-tarball + + tar.gz + + true + + + + + + true + + com.datastax.oss:java-driver-core + + + lib/core + false + + + lib/core + + + com.datastax.oss:java-driver-query-builder + + true + + + + + + + + true + + com.datastax.oss:java-driver-query-builder + + + lib/query-builder + false + + + + com.datastax.oss:java-driver-core + + com.datastax.oss:java-driver-shaded-guava + com.github.stephenc.jcip:jcip-annotations + + + true + + + + + + + + true + + com.datastax.oss:java-driver-core + com.datastax.oss:java-driver-query-builder + + + false + sources + ${module.artifactId}-${module.version}-src.zip + + src + + * + + + + + + + + + + target/apidocs + apidocs + + + + .. + . + + README* + LICENSE* + + + + + ../changelog + + + + ../faq + + + + ../manual + + + + ../upgrade_guide + + + + + diff --git a/docs.yaml b/docs.yaml index b38b86397d7..17989d6312f 100644 --- a/docs.yaml +++ b/docs.yaml @@ -1,6 +1,6 @@ -title: Java Driver for Apache Cassandra -summary: High performance Java client for Apache Cassandra -homepage: http://datastax.github.io/java-driver/ +title: Java Driver for Apache Cassandra™ +summary: High performance Java client for Apache Cassandra™ +homepage: http://docs.datastax.com/en/developer/java-driver theme: datastax sections: - title: Manual @@ -8,14 +8,11 @@ sections: sources: - type: markdown files: 'manual/**/*.md' - # The 'manual' section was called 'features' in older releases. Leave both - # definitions and Documentor will pick up whichever exists and ignore the - # other. - - title: Features - prefix: /features + - title: Reference configuration + prefix: /manual/core/configuration/reference sources: - - type: markdown - files: 'features/**/*.md' + - type: rst + files: 'manual/core/configuration/reference/*.rst' - title: Changelog prefix: /changelog sources: @@ -35,33 +32,12 @@ links: - title: Code href: https://github.com/datastax/java-driver/ - title: Docs - href: http://docs.datastax.com/en/developer/java-driver/ + href: http://docs.datastax.com/en/developer/java-driver - title: Issues href: https://datastax-oss.atlassian.net/browse/JAVA/ - title: Mailing List href: https://groups.google.com/a/lists.datastax.com/forum/#!forum/java-driver-user - title: Releases - href: http://downloads.datastax.com/java-driver/ + href: https://github.com/datastax/java-driver/releases api_docs: - 3.3: http://docs.datastax.com/en/drivers/java/3.3 - 4.0-alpha: http://docs.datastax.com/en/drivers/java/4.0 - 3.2: http://docs.datastax.com/en/drivers/java/3.2 - 3.1: http://docs.datastax.com/en/drivers/java/3.1 - 3.0: http://docs.datastax.com/en/drivers/java/3.0 - 2.1: http://docs.datastax.com/en/drivers/java/2.1 - 2.0: http://docs.datastax.com/en/drivers/java/2.0 -versions: - - name: '3.3' - ref: '3.3.0' - - name: '4.0-alpha' - ref: '9f0edeb' - - name: '3.2' - ref: '3.2_docfixes' - - name: '3.1' - ref: '3.1_docfixes' - - name: '3.0' - ref: '3.0_docfixes' - - name: '2.1' - ref: '2.1.10.3' - - name: '2.0' - ref: '2.0.12.3' + 4.0: http://docs.datastax.com/en/drivers/java/4.0 diff --git a/doxyfile b/doxyfile deleted file mode 100644 index 414bdbd7ec4..00000000000 --- a/doxyfile +++ /dev/null @@ -1,336 +0,0 @@ -# Doxyfile 1.8.10 - -#--------------------------------------------------------------------------- -# Project related configuration options -#--------------------------------------------------------------------------- -DOXYFILE_ENCODING = UTF-8 -PROJECT_NAME = "DataStax Java Driver" -PROJECT_NUMBER = -PROJECT_BRIEF = -PROJECT_LOGO = -OUTPUT_DIRECTORY = -CREATE_SUBDIRS = NO -ALLOW_UNICODE_NAMES = NO -OUTPUT_LANGUAGE = English -BRIEF_MEMBER_DESC = YES -REPEAT_BRIEF = YES -ABBREVIATE_BRIEF = "The $name class" \ - "The $name widget" \ - "The $name file" \ - is \ - provides \ - specifies \ - contains \ - represents \ - a \ - an \ - the -ALWAYS_DETAILED_SEC = NO -INLINE_INHERITED_MEMB = NO -FULL_PATH_NAMES = NO -STRIP_FROM_PATH = -STRIP_FROM_INC_PATH = -SHORT_NAMES = NO -JAVADOC_AUTOBRIEF = NO -QT_AUTOBRIEF = NO -MULTILINE_CPP_IS_BRIEF = NO -INHERIT_DOCS = YES -SEPARATE_MEMBER_PAGES = NO -TAB_SIZE = 4 -ALIASES = "throws=\par Throws\n" \ - "test_assumptions=\par Test Assumptions\n" \ - "note=\par Note\n" \ - "test_category=\par Test Category\n" \ - "jira_ticket=\par JIRA Ticket\n" \ - "expected_result=\par Expected Result\n" \ - "since=\par Since\n" \ - "param=\par Parameters\n" \ - "return=\par Return\n" \ - "expected_errors=\par Expected Errors\n" -TCL_SUBST = -OPTIMIZE_OUTPUT_FOR_C = NO -OPTIMIZE_OUTPUT_JAVA = YES -OPTIMIZE_FOR_FORTRAN = NO -OPTIMIZE_OUTPUT_VHDL = NO -EXTENSION_MAPPING = -MARKDOWN_SUPPORT = YES -AUTOLINK_SUPPORT = YES -BUILTIN_STL_SUPPORT = NO -CPP_CLI_SUPPORT = NO -SIP_SUPPORT = NO -IDL_PROPERTY_SUPPORT = YES -DISTRIBUTE_GROUP_DOC = NO -GROUP_NESTED_COMPOUNDS = NO -SUBGROUPING = YES -INLINE_GROUPED_CLASSES = NO -INLINE_SIMPLE_STRUCTS = NO -TYPEDEF_HIDES_STRUCT = NO -LOOKUP_CACHE_SIZE = 0 -#--------------------------------------------------------------------------- -# Build related configuration options -#--------------------------------------------------------------------------- -EXTRACT_ALL = NO -EXTRACT_PRIVATE = NO -EXTRACT_PACKAGE = NO -EXTRACT_STATIC = NO -EXTRACT_LOCAL_CLASSES = YES -EXTRACT_LOCAL_METHODS = NO -EXTRACT_ANON_NSPACES = NO -HIDE_UNDOC_MEMBERS = NO -HIDE_UNDOC_CLASSES = NO -HIDE_FRIEND_COMPOUNDS = NO -HIDE_IN_BODY_DOCS = NO -INTERNAL_DOCS = NO -CASE_SENSE_NAMES = NO -HIDE_SCOPE_NAMES = NO -HIDE_COMPOUND_REFERENCE= NO -SHOW_INCLUDE_FILES = YES -SHOW_GROUPED_MEMB_INC = NO -FORCE_LOCAL_INCLUDES = NO -INLINE_INFO = YES -SORT_MEMBER_DOCS = YES -SORT_BRIEF_DOCS = NO -SORT_MEMBERS_CTORS_1ST = NO -SORT_GROUP_NAMES = NO -SORT_BY_SCOPE_NAME = NO -STRICT_PROTO_MATCHING = NO -GENERATE_TODOLIST = YES -GENERATE_TESTLIST = YES -GENERATE_BUGLIST = YES -GENERATE_DEPRECATEDLIST= YES -ENABLED_SECTIONS = -MAX_INITIALIZER_LINES = 30 -SHOW_USED_FILES = YES -SHOW_FILES = YES -SHOW_NAMESPACES = YES -FILE_VERSION_FILTER = -LAYOUT_FILE = -CITE_BIB_FILES = -#--------------------------------------------------------------------------- -# Configuration options related to warning and progress messages -#--------------------------------------------------------------------------- -QUIET = NO -WARNINGS = YES -WARN_IF_UNDOCUMENTED = YES -WARN_IF_DOC_ERROR = YES -WARN_NO_PARAMDOC = NO -WARN_FORMAT = "$file:$line: $text" -WARN_LOGFILE = -#--------------------------------------------------------------------------- -# Configuration options related to the input files -#--------------------------------------------------------------------------- -INPUT = ./driver-core/src/test -INPUT_ENCODING = UTF-8 -FILE_PATTERNS = *.java -RECURSIVE = YES -EXCLUDE = @Test -EXCLUDE_SYMLINKS = NO -EXCLUDE_PATTERNS = -EXCLUDE_SYMBOLS = -EXAMPLE_PATH = -EXAMPLE_PATTERNS = * -EXAMPLE_RECURSIVE = NO -IMAGE_PATH = -INPUT_FILTER = -FILTER_PATTERNS = -FILTER_SOURCE_FILES = NO -FILTER_SOURCE_PATTERNS = -USE_MDFILE_AS_MAINPAGE = -#--------------------------------------------------------------------------- -# Configuration options related to source browsing -#--------------------------------------------------------------------------- -SOURCE_BROWSER = NO -INLINE_SOURCES = NO -STRIP_CODE_COMMENTS = YES -REFERENCED_BY_RELATION = NO -REFERENCES_RELATION = NO -REFERENCES_LINK_SOURCE = YES -SOURCE_TOOLTIPS = YES -USE_HTAGS = NO -VERBATIM_HEADERS = YES -CLANG_ASSISTED_PARSING = NO -CLANG_OPTIONS = -#--------------------------------------------------------------------------- -# Configuration options related to the alphabetical class index -#--------------------------------------------------------------------------- -ALPHABETICAL_INDEX = YES -COLS_IN_ALPHA_INDEX = 5 -IGNORE_PREFIX = -#--------------------------------------------------------------------------- -# Configuration options related to the HTML output -#--------------------------------------------------------------------------- -GENERATE_HTML = YES -HTML_OUTPUT = html -HTML_FILE_EXTENSION = .html -HTML_HEADER = -HTML_FOOTER = -HTML_STYLESHEET = -HTML_EXTRA_STYLESHEET = -HTML_EXTRA_FILES = -HTML_COLORSTYLE_HUE = 220 -HTML_COLORSTYLE_SAT = 100 -HTML_COLORSTYLE_GAMMA = 80 -HTML_TIMESTAMP = YES -HTML_DYNAMIC_SECTIONS = NO -HTML_INDEX_NUM_ENTRIES = 100 -GENERATE_DOCSET = NO -DOCSET_FEEDNAME = "Doxygen generated docs" -DOCSET_BUNDLE_ID = org.doxygen.Project -DOCSET_PUBLISHER_ID = org.doxygen.Publisher -DOCSET_PUBLISHER_NAME = Publisher -GENERATE_HTMLHELP = NO -CHM_FILE = -HHC_LOCATION = -GENERATE_CHI = NO -CHM_INDEX_ENCODING = -BINARY_TOC = NO -TOC_EXPAND = NO -GENERATE_QHP = NO -QCH_FILE = -QHP_NAMESPACE = org.doxygen.Project -QHP_VIRTUAL_FOLDER = doc -QHP_CUST_FILTER_NAME = -QHP_CUST_FILTER_ATTRS = -QHP_SECT_FILTER_ATTRS = -QHG_LOCATION = -GENERATE_ECLIPSEHELP = NO -ECLIPSE_DOC_ID = org.doxygen.Project -DISABLE_INDEX = NO -GENERATE_TREEVIEW = YES -ENUM_VALUES_PER_LINE = 4 -TREEVIEW_WIDTH = 250 -EXT_LINKS_IN_WINDOW = NO -FORMULA_FONTSIZE = 10 -FORMULA_TRANSPARENT = YES -USE_MATHJAX = NO -MATHJAX_FORMAT = HTML-CSS -MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest -MATHJAX_EXTENSIONS = -MATHJAX_CODEFILE = -SEARCHENGINE = YES -SERVER_BASED_SEARCH = NO -EXTERNAL_SEARCH = NO -SEARCHENGINE_URL = -SEARCHDATA_FILE = searchdata.xml -EXTERNAL_SEARCH_ID = -EXTRA_SEARCH_MAPPINGS = -#--------------------------------------------------------------------------- -# Configuration options related to the LaTeX output -#--------------------------------------------------------------------------- -GENERATE_LATEX = NO -LATEX_OUTPUT = latex -LATEX_CMD_NAME = latex -MAKEINDEX_CMD_NAME = makeindex -COMPACT_LATEX = NO -PAPER_TYPE = a4 -EXTRA_PACKAGES = -LATEX_HEADER = -LATEX_FOOTER = -LATEX_EXTRA_STYLESHEET = -LATEX_EXTRA_FILES = -PDF_HYPERLINKS = YES -USE_PDFLATEX = YES -LATEX_BATCHMODE = NO -LATEX_HIDE_INDICES = NO -LATEX_SOURCE_CODE = NO -LATEX_BIB_STYLE = plain -#--------------------------------------------------------------------------- -# Configuration options related to the RTF output -#--------------------------------------------------------------------------- -GENERATE_RTF = NO -RTF_OUTPUT = rtf -COMPACT_RTF = NO -RTF_HYPERLINKS = NO -RTF_STYLESHEET_FILE = -RTF_EXTENSIONS_FILE = -RTF_SOURCE_CODE = NO -#--------------------------------------------------------------------------- -# Configuration options related to the man page output -#--------------------------------------------------------------------------- -GENERATE_MAN = NO -MAN_OUTPUT = man -MAN_EXTENSION = .3 -MAN_SUBDIR = -MAN_LINKS = NO -#--------------------------------------------------------------------------- -# Configuration options related to the XML output -#--------------------------------------------------------------------------- -GENERATE_XML = NO -XML_OUTPUT = xml -XML_PROGRAMLISTING = YES -#--------------------------------------------------------------------------- -# Configuration options related to the DOCBOOK output -#--------------------------------------------------------------------------- -GENERATE_DOCBOOK = NO -DOCBOOK_OUTPUT = docbook -DOCBOOK_PROGRAMLISTING = NO -#--------------------------------------------------------------------------- -# Configuration options for the AutoGen Definitions output -#--------------------------------------------------------------------------- -GENERATE_AUTOGEN_DEF = NO -#--------------------------------------------------------------------------- -# Configuration options related to the Perl module output -#--------------------------------------------------------------------------- -GENERATE_PERLMOD = NO -PERLMOD_LATEX = NO -PERLMOD_PRETTY = YES -PERLMOD_MAKEVAR_PREFIX = -#--------------------------------------------------------------------------- -# Configuration options related to the preprocessor -#--------------------------------------------------------------------------- -ENABLE_PREPROCESSING = YES -MACRO_EXPANSION = NO -EXPAND_ONLY_PREDEF = NO -SEARCH_INCLUDES = YES -INCLUDE_PATH = -INCLUDE_FILE_PATTERNS = -PREDEFINED = -EXPAND_AS_DEFINED = -SKIP_FUNCTION_MACROS = YES -#--------------------------------------------------------------------------- -# Configuration options related to external references -#--------------------------------------------------------------------------- -TAGFILES = -GENERATE_TAGFILE = -ALLEXTERNALS = NO -EXTERNAL_GROUPS = YES -EXTERNAL_PAGES = YES -PERL_PATH = /usr/bin/perl -#--------------------------------------------------------------------------- -# Configuration options related to the dot tool -#--------------------------------------------------------------------------- -CLASS_DIAGRAMS = YES -MSCGEN_PATH = -DIA_PATH = -HIDE_UNDOC_RELATIONS = YES -HAVE_DOT = NO -DOT_NUM_THREADS = 0 -DOT_FONTNAME = Helvetica -DOT_FONTSIZE = 10 -DOT_FONTPATH = -CLASS_GRAPH = YES -COLLABORATION_GRAPH = YES -GROUP_GRAPHS = YES -UML_LOOK = NO -UML_LIMIT_NUM_FIELDS = 10 -TEMPLATE_RELATIONS = NO -INCLUDE_GRAPH = YES -INCLUDED_BY_GRAPH = YES -CALL_GRAPH = NO -CALLER_GRAPH = NO -GRAPHICAL_HIERARCHY = YES -DIRECTORY_GRAPH = YES -DOT_IMAGE_FORMAT = png -INTERACTIVE_SVG = NO -DOT_PATH = -DOTFILE_DIRS = -MSCFILE_DIRS = -DIAFILE_DIRS = -PLANTUML_JAR_PATH = -PLANTUML_INCLUDE_PATH = -DOT_GRAPH_MAX_NODES = 50 -MAX_DOT_GRAPH_DEPTH = 0 -DOT_TRANSPARENT = NO -DOT_MULTI_TARGETS = NO -GENERATE_LEGEND = YES -DOT_CLEANUP = YES \ No newline at end of file diff --git a/driver-core/pom.xml b/driver-core/pom.xml deleted file mode 100644 index 1db54e06725..00000000000 --- a/driver-core/pom.xml +++ /dev/null @@ -1,358 +0,0 @@ - - - - 4.0.0 - - - com.datastax.cassandra - cassandra-driver-parent - 3.11.5 - - - cassandra-driver-core - DataStax Java Driver for Apache Cassandra - Core - - A driver for Apache Cassandra 1.2+ that works exclusively with the Cassandra Query Language version 3 - (CQL3) and Cassandra's binary protocol. - - - - - - io.netty - netty-handler - - - - com.google.guava - guava - - - - io.dropwizard.metrics - metrics-core - - - - org.slf4j - slf4j-api - - - - com.github.jnr - jnr-ffi - - - - com.github.jnr - jnr-posix - - - - - - - org.xerial.snappy - snappy-java - true - - - - org.lz4 - lz4-java - true - - - - - - io.netty - netty-transport-native-epoll - true - - - - org.hdrhistogram - HdrHistogram - true - - - - org.testng - testng - test - - - - org.assertj - assertj-core - test - - - - org.mockito - mockito-all - test - - - - org.scassandra - java-client - test - - - - org.apache.commons - commons-exec - test - - - - io.netty - ${netty-tcnative.artifact} - ${os.detected.classifier} - test - - - - log4j - log4j - test - - - - org.slf4j - slf4j-log4j12 - test - - - - com.github.tomakehurst - wiremock - test - - - - org.jboss.byteman - byteman-bmunit - test - ${byteman.version} - - - org.testng - testng - - - - - com.fasterxml.jackson.core - jackson-databind - - - - - - - - - src/main/resources - true - - - - - - - maven-jar-plugin - - - ${project.build.outputDirectory}/META-INF/MANIFEST.MF - - - - - test-jar - - test-jar - - - - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-test-sources - - test-jar-no-fork - - - - - - org.apache.felix - maven-bundle-plugin - - - com.datastax.driver.core - - - - true - - - - - - bundle-manifest - process-classes - - manifest - - - ${project.build.outputDirectory}/META-INF - - - - - - - - - bundle-manifest-shaded - process-classes - - manifest - - - ${project.build.directory}/META-INF-shaded - - - - com.datastax.shaded.* - com.datastax.shaded.metrics;* - - - - - - - - maven-shade-plugin - - true - - - io.netty:* - io.dropwizard.metrics:metrics-core - - - io.netty:netty-transport-native-epoll - - - - - io.netty - com.datastax.shaded.netty - - - com.codahale.metrics - com.datastax.shaded.metrics - - - - - - META-INF/MANIFEST.MF - META-INF/io.netty.versions.properties - META-INF/maven/io.netty/netty-buffer/pom.properties - META-INF/maven/io.netty/netty-buffer/pom.xml - META-INF/maven/io.netty/netty-codec/pom.properties - META-INF/maven/io.netty/netty-codec/pom.xml - META-INF/maven/io.netty/netty-common/pom.properties - META-INF/maven/io.netty/netty-common/pom.xml - META-INF/maven/io.netty/netty-handler/pom.properties - META-INF/maven/io.netty/netty-handler/pom.xml - META-INF/maven/io.netty/netty-transport/pom.properties - META-INF/maven/io.netty/netty-transport/pom.xml - META-INF/maven/io.dropwizard.metrics/metrics-core/pom.properties - META-INF/maven/io.dropwizard.metrics/metrics-core/pom.xml - - - - - META-INF/MANIFEST.MF - ${project.build.directory}/META-INF-shaded/MANIFEST.MF - - - - - - package - - shade - - - - - - - - - - - - - isolated - - - - maven-surefire-plugin - - false - - **/SSL*Test.java - **/ControlConnectionTest.java - **/ExtendedPeerCheckDisabledTest.java - **/UUIDsPID*.java - **/FrameLengthTest.java - **/HeapCompressionTest.java - - - - - - - - - - - diff --git a/driver-core/src/main/java/com/datastax/driver/core/AbstractAddressableByIndexData.java b/driver-core/src/main/java/com/datastax/driver/core/AbstractAddressableByIndexData.java deleted file mode 100644 index ea05bf92f2e..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AbstractAddressableByIndexData.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.reflect.TypeToken; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -abstract class AbstractAddressableByIndexData> - extends AbstractGettableByIndexData implements SettableByIndexData { - - final ByteBuffer[] values; - - protected AbstractAddressableByIndexData(ProtocolVersion protocolVersion, int size) { - super(protocolVersion); - this.values = new ByteBuffer[size]; - } - - @SuppressWarnings("unchecked") - protected T setValue(int i, ByteBuffer value) { - values[i] = value; - return (T) this; - } - - @Override - protected ByteBuffer getValue(int i) { - return values[i]; - } - - @Override - public T setBool(int i, boolean v) { - TypeCodec codec = codecFor(i, Boolean.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveBooleanCodec) - bb = ((TypeCodec.PrimitiveBooleanCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setByte(int i, byte v) { - TypeCodec codec = codecFor(i, Byte.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveByteCodec) - bb = ((TypeCodec.PrimitiveByteCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setShort(int i, short v) { - TypeCodec codec = codecFor(i, Short.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveShortCodec) - bb = ((TypeCodec.PrimitiveShortCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setInt(int i, int v) { - TypeCodec codec = codecFor(i, Integer.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveIntCodec) - bb = ((TypeCodec.PrimitiveIntCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setLong(int i, long v) { - TypeCodec codec = codecFor(i, Long.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveLongCodec) - bb = ((TypeCodec.PrimitiveLongCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setTimestamp(int i, Date v) { - return setValue(i, codecFor(i, Date.class).serialize(v, protocolVersion)); - } - - @Override - public T setDate(int i, LocalDate v) { - return setValue(i, codecFor(i, LocalDate.class).serialize(v, protocolVersion)); - } - - @Override - public T setTime(int i, long v) { - TypeCodec codec = codecFor(i, Long.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveLongCodec) - bb = ((TypeCodec.PrimitiveLongCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setFloat(int i, float v) { - TypeCodec codec = codecFor(i, Float.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveFloatCodec) - bb = ((TypeCodec.PrimitiveFloatCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setDouble(int i, double v) { - TypeCodec codec = codecFor(i, Double.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveDoubleCodec) - bb = ((TypeCodec.PrimitiveDoubleCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setString(int i, String v) { - return setValue(i, codecFor(i, String.class).serialize(v, protocolVersion)); - } - - @Override - public T setBytes(int i, ByteBuffer v) { - return setValue(i, codecFor(i, ByteBuffer.class).serialize(v, protocolVersion)); - } - - @Override - public T setBytesUnsafe(int i, ByteBuffer v) { - return setValue(i, v == null ? null : v.duplicate()); - } - - @Override - public T setVarint(int i, BigInteger v) { - return setValue(i, codecFor(i, BigInteger.class).serialize(v, protocolVersion)); - } - - @Override - public T setDecimal(int i, BigDecimal v) { - return setValue(i, codecFor(i, BigDecimal.class).serialize(v, protocolVersion)); - } - - @Override - public T setUUID(int i, UUID v) { - return setValue(i, codecFor(i, UUID.class).serialize(v, protocolVersion)); - } - - @Override - public T setInet(int i, InetAddress v) { - return setValue(i, codecFor(i, InetAddress.class).serialize(v, protocolVersion)); - } - - @Override - @SuppressWarnings("unchecked") - public T setList(int i, List v) { - return setValue(i, codecFor(i).serialize(v, protocolVersion)); - } - - @Override - public T setList(int i, List v, Class elementsClass) { - return setValue(i, codecFor(i, TypeTokens.listOf(elementsClass)).serialize(v, protocolVersion)); - } - - @Override - public T setList(int i, List v, TypeToken elementsType) { - return setValue(i, codecFor(i, TypeTokens.listOf(elementsType)).serialize(v, protocolVersion)); - } - - @Override - @SuppressWarnings("unchecked") - public T setMap(int i, Map v) { - return setValue(i, codecFor(i).serialize(v, protocolVersion)); - } - - @Override - public T setMap(int i, Map v, Class keysClass, Class valuesClass) { - return setValue( - i, codecFor(i, TypeTokens.mapOf(keysClass, valuesClass)).serialize(v, protocolVersion)); - } - - @Override - public T setMap(int i, Map v, TypeToken keysType, TypeToken valuesType) { - return setValue( - i, codecFor(i, TypeTokens.mapOf(keysType, valuesType)).serialize(v, protocolVersion)); - } - - @Override - @SuppressWarnings("unchecked") - public T setSet(int i, Set v) { - return setValue(i, codecFor(i).serialize(v, protocolVersion)); - } - - @Override - public T setSet(int i, Set v, Class elementsClass) { - return setValue(i, codecFor(i, TypeTokens.setOf(elementsClass)).serialize(v, protocolVersion)); - } - - @Override - public T setSet(int i, Set v, TypeToken elementsType) { - return setValue(i, codecFor(i, TypeTokens.setOf(elementsType)).serialize(v, protocolVersion)); - } - - @Override - public T setUDTValue(int i, UDTValue v) { - return setValue(i, codecFor(i, UDTValue.class).serialize(v, protocolVersion)); - } - - @Override - public T setTupleValue(int i, TupleValue v) { - return setValue(i, codecFor(i, TupleValue.class).serialize(v, protocolVersion)); - } - - @Override - public T set(int i, V v, Class targetClass) { - return set(i, v, codecFor(i, targetClass)); - } - - @Override - public T set(int i, V v, TypeToken targetType) { - return set(i, v, codecFor(i, targetType)); - } - - @Override - public T set(int i, V v, TypeCodec codec) { - checkType(i, codec.getCqlType().getName()); - return setValue(i, codec.serialize(v, protocolVersion)); - } - - @Override - public T setToNull(int i) { - return setValue(i, null); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof AbstractAddressableByIndexData)) return false; - - AbstractAddressableByIndexData that = (AbstractAddressableByIndexData) o; - if (values.length != that.values.length) return false; - - if (this.protocolVersion != that.protocolVersion) return false; - - // Deserializing each value is slightly inefficient, but comparing - // the bytes could in theory be wrong (for varint for instance, 2 values - // can have different binary representation but be the same value due to - // leading zeros). So we don't take any risk. - for (int i = 0; i < values.length; i++) { - DataType thisType = getType(i); - DataType thatType = that.getType(i); - if (!thisType.equals(thatType)) return false; - - Object thisValue = this.codecFor(i).deserialize(this.values[i], this.protocolVersion); - Object thatValue = that.codecFor(i).deserialize(that.values[i], that.protocolVersion); - if (!MoreObjects.equal(thisValue, thatValue)) return false; - } - return true; - } - - @Override - public int hashCode() { - // Same as equals - int hash = 31; - for (int i = 0; i < values.length; i++) - hash += - values[i] == null ? 1 : codecFor(i).deserialize(values[i], protocolVersion).hashCode(); - return hash; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/AbstractData.java b/driver-core/src/main/java/com/datastax/driver/core/AbstractData.java deleted file mode 100644 index 8013729f65d..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AbstractData.java +++ /dev/null @@ -1,588 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.reflect.TypeToken; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -// We don't want to expose this one: it's less useful externally and it's a bit ugly to expose -// anyway (but it's convenient). -abstract class AbstractData> extends AbstractGettableData - implements SettableData { - - final T wrapped; - final ByteBuffer[] values; - - // Ugly, we could probably clean that: it is currently needed however because we sometimes - // want wrapped to be 'this' (UDTValue), and sometimes some other object (in BoundStatement). - @SuppressWarnings("unchecked") - protected AbstractData(ProtocolVersion protocolVersion, int size) { - super(protocolVersion); - this.wrapped = (T) this; - this.values = new ByteBuffer[size]; - } - - protected AbstractData(ProtocolVersion protocolVersion, T wrapped, int size) { - this(protocolVersion, wrapped, new ByteBuffer[size]); - } - - protected AbstractData(ProtocolVersion protocolVersion, T wrapped, ByteBuffer[] values) { - super(protocolVersion); - this.wrapped = wrapped; - this.values = values; - } - - protected abstract int[] getAllIndexesOf(String name); - - protected T setValue(int i, ByteBuffer value) { - values[i] = value; - return wrapped; - } - - @Override - protected ByteBuffer getValue(int i) { - return values[i]; - } - - @Override - protected int getIndexOf(String name) { - return getAllIndexesOf(name)[0]; - } - - @Override - public T setBool(int i, boolean v) { - TypeCodec codec = codecFor(i, Boolean.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveBooleanCodec) - bb = ((TypeCodec.PrimitiveBooleanCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setBool(String name, boolean v) { - for (int i : getAllIndexesOf(name)) { - setBool(i, v); - } - return wrapped; - } - - @Override - public T setByte(int i, byte v) { - TypeCodec codec = codecFor(i, Byte.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveByteCodec) - bb = ((TypeCodec.PrimitiveByteCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setByte(String name, byte v) { - for (int i : getAllIndexesOf(name)) { - setByte(i, v); - } - return wrapped; - } - - @Override - public T setShort(int i, short v) { - TypeCodec codec = codecFor(i, Short.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveShortCodec) - bb = ((TypeCodec.PrimitiveShortCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setShort(String name, short v) { - for (int i : getAllIndexesOf(name)) { - setShort(i, v); - } - return wrapped; - } - - @Override - public T setInt(int i, int v) { - TypeCodec codec = codecFor(i, Integer.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveIntCodec) - bb = ((TypeCodec.PrimitiveIntCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setInt(String name, int v) { - for (int i : getAllIndexesOf(name)) { - setInt(i, v); - } - return wrapped; - } - - @Override - public T setLong(int i, long v) { - TypeCodec codec = codecFor(i, Long.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveLongCodec) - bb = ((TypeCodec.PrimitiveLongCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setLong(String name, long v) { - for (int i : getAllIndexesOf(name)) { - setLong(i, v); - } - return wrapped; - } - - @Override - public T setTimestamp(int i, Date v) { - return setValue(i, codecFor(i, Date.class).serialize(v, protocolVersion)); - } - - @Override - public T setTimestamp(String name, Date v) { - for (int i : getAllIndexesOf(name)) { - setTimestamp(i, v); - } - return wrapped; - } - - @Override - public T setDate(int i, LocalDate v) { - return setValue(i, codecFor(i, LocalDate.class).serialize(v, protocolVersion)); - } - - @Override - public T setDate(String name, LocalDate v) { - for (int i : getAllIndexesOf(name)) { - setDate(i, v); - } - return wrapped; - } - - @Override - public T setTime(int i, long v) { - TypeCodec codec = codecFor(i, Long.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveLongCodec) - bb = ((TypeCodec.PrimitiveLongCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setTime(String name, long v) { - for (int i : getAllIndexesOf(name)) { - setTime(i, v); - } - return wrapped; - } - - @Override - public T setFloat(int i, float v) { - TypeCodec codec = codecFor(i, Float.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveFloatCodec) - bb = ((TypeCodec.PrimitiveFloatCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setFloat(String name, float v) { - for (int i : getAllIndexesOf(name)) { - setFloat(i, v); - } - return wrapped; - } - - @Override - public T setDouble(int i, double v) { - TypeCodec codec = codecFor(i, Double.class); - ByteBuffer bb; - if (codec instanceof TypeCodec.PrimitiveDoubleCodec) - bb = ((TypeCodec.PrimitiveDoubleCodec) codec).serializeNoBoxing(v, protocolVersion); - else bb = codec.serialize(v, protocolVersion); - return setValue(i, bb); - } - - @Override - public T setDouble(String name, double v) { - for (int i : getAllIndexesOf(name)) { - setDouble(i, v); - } - return wrapped; - } - - @Override - public T setString(int i, String v) { - return setValue(i, codecFor(i, String.class).serialize(v, protocolVersion)); - } - - @Override - public T setString(String name, String v) { - for (int i : getAllIndexesOf(name)) { - setString(i, v); - } - return wrapped; - } - - @Override - public T setBytes(int i, ByteBuffer v) { - return setValue(i, codecFor(i, ByteBuffer.class).serialize(v, protocolVersion)); - } - - @Override - public T setBytes(String name, ByteBuffer v) { - for (int i : getAllIndexesOf(name)) { - setBytes(i, v); - } - return wrapped; - } - - @Override - public T setBytesUnsafe(int i, ByteBuffer v) { - return setValue(i, v == null ? null : v.duplicate()); - } - - @Override - public T setBytesUnsafe(String name, ByteBuffer v) { - ByteBuffer value = v == null ? null : v.duplicate(); - for (int i : getAllIndexesOf(name)) { - setValue(i, value); - } - return wrapped; - } - - @Override - public T setVarint(int i, BigInteger v) { - return setValue(i, codecFor(i, BigInteger.class).serialize(v, protocolVersion)); - } - - @Override - public T setVarint(String name, BigInteger v) { - for (int i : getAllIndexesOf(name)) { - setVarint(i, v); - } - return wrapped; - } - - @Override - public T setDecimal(int i, BigDecimal v) { - return setValue(i, codecFor(i, BigDecimal.class).serialize(v, protocolVersion)); - } - - @Override - public T setDecimal(String name, BigDecimal v) { - for (int i : getAllIndexesOf(name)) { - setDecimal(i, v); - } - return wrapped; - } - - @Override - public T setUUID(int i, UUID v) { - return setValue(i, codecFor(i, UUID.class).serialize(v, protocolVersion)); - } - - @Override - public T setUUID(String name, UUID v) { - for (int i : getAllIndexesOf(name)) { - setUUID(i, v); - } - return wrapped; - } - - @Override - public T setInet(int i, InetAddress v) { - return setValue(i, codecFor(i, InetAddress.class).serialize(v, protocolVersion)); - } - - @Override - public T setInet(String name, InetAddress v) { - for (int i : getAllIndexesOf(name)) { - setInet(i, v); - } - return wrapped; - } - - // setToken is package-private because we only want to expose it in BoundStatement - T setToken(int i, Token v) { - if (v == null) - throw new NullPointerException( - String.format("Cannot set a null token for column %s", getName(i))); - checkType(i, v.getType().getName()); - // Bypass CodecRegistry when serializing tokens - return setValue(i, v.serialize(protocolVersion)); - } - - T setToken(String name, Token v) { - for (int i : getAllIndexesOf(name)) { - setToken(i, v); - } - return wrapped; - } - - @Override - @SuppressWarnings("unchecked") - public T setList(int i, List v) { - return setValue(i, codecFor(i).serialize(v, protocolVersion)); - } - - @Override - public T setList(int i, List v, Class elementsClass) { - return setValue(i, codecFor(i, TypeTokens.listOf(elementsClass)).serialize(v, protocolVersion)); - } - - @Override - public T setList(int i, List v, TypeToken elementsType) { - return setValue(i, codecFor(i, TypeTokens.listOf(elementsType)).serialize(v, protocolVersion)); - } - - @Override - public T setList(String name, List v) { - for (int i : getAllIndexesOf(name)) { - setList(i, v); - } - return wrapped; - } - - @Override - public T setList(String name, List v, Class elementsClass) { - for (int i : getAllIndexesOf(name)) { - setList(i, v, elementsClass); - } - return wrapped; - } - - @Override - public T setList(String name, List v, TypeToken elementsType) { - for (int i : getAllIndexesOf(name)) { - setList(i, v, elementsType); - } - return wrapped; - } - - @SuppressWarnings("unchecked") - @Override - public T setMap(int i, Map v) { - return setValue(i, codecFor(i).serialize(v, protocolVersion)); - } - - @Override - public T setMap(int i, Map v, Class keysClass, Class valuesClass) { - return setValue( - i, codecFor(i, TypeTokens.mapOf(keysClass, valuesClass)).serialize(v, protocolVersion)); - } - - @Override - public T setMap(int i, Map v, TypeToken keysType, TypeToken valuesType) { - return setValue( - i, codecFor(i, TypeTokens.mapOf(keysType, valuesType)).serialize(v, protocolVersion)); - } - - @Override - public T setMap(String name, Map v) { - for (int i : getAllIndexesOf(name)) { - setMap(i, v); - } - return wrapped; - } - - @Override - public T setMap(String name, Map v, Class keysClass, Class valuesClass) { - for (int i : getAllIndexesOf(name)) { - setMap(i, v, keysClass, valuesClass); - } - return wrapped; - } - - @Override - public T setMap(String name, Map v, TypeToken keysType, TypeToken valuesType) { - for (int i : getAllIndexesOf(name)) { - setMap(i, v, keysType, valuesType); - } - return wrapped; - } - - @Override - @SuppressWarnings("unchecked") - public T setSet(int i, Set v) { - return setValue(i, codecFor(i).serialize(v, protocolVersion)); - } - - @Override - public T setSet(int i, Set v, Class elementsClass) { - return setValue(i, codecFor(i, TypeTokens.setOf(elementsClass)).serialize(v, protocolVersion)); - } - - @Override - public T setSet(int i, Set v, TypeToken elementsType) { - return setValue(i, codecFor(i, TypeTokens.setOf(elementsType)).serialize(v, protocolVersion)); - } - - @Override - public T setSet(String name, Set v) { - for (int i : getAllIndexesOf(name)) { - setSet(i, v); - } - return wrapped; - } - - @Override - public T setSet(String name, Set v, Class elementsClass) { - for (int i : getAllIndexesOf(name)) { - setSet(i, v, elementsClass); - } - return wrapped; - } - - @Override - public T setSet(String name, Set v, TypeToken elementsType) { - for (int i : getAllIndexesOf(name)) { - setSet(i, v, elementsType); - } - return wrapped; - } - - @Override - public T setUDTValue(int i, UDTValue v) { - return setValue(i, codecFor(i, UDTValue.class).serialize(v, protocolVersion)); - } - - @Override - public T setUDTValue(String name, UDTValue v) { - for (int i : getAllIndexesOf(name)) { - setUDTValue(i, v); - } - return wrapped; - } - - @Override - public T setTupleValue(int i, TupleValue v) { - return setValue(i, codecFor(i, TupleValue.class).serialize(v, protocolVersion)); - } - - @Override - public T setTupleValue(String name, TupleValue v) { - for (int i : getAllIndexesOf(name)) { - setTupleValue(i, v); - } - return wrapped; - } - - @Override - public T set(int i, V v, Class targetClass) { - return set(i, v, codecFor(i, targetClass)); - } - - @Override - public T set(String name, V v, Class targetClass) { - for (int i : getAllIndexesOf(name)) { - set(i, v, targetClass); - } - return wrapped; - } - - @Override - public T set(int i, V v, TypeToken targetType) { - return set(i, v, codecFor(i, targetType)); - } - - @Override - public T set(String name, V v, TypeToken targetType) { - for (int i : getAllIndexesOf(name)) { - set(i, v, targetType); - } - return wrapped; - } - - @Override - public T set(int i, V v, TypeCodec codec) { - checkType(i, codec.getCqlType().getName()); - return setValue(i, codec.serialize(v, protocolVersion)); - } - - @Override - public T set(String name, V v, TypeCodec codec) { - for (int i : getAllIndexesOf(name)) { - set(i, v, codec); - } - return wrapped; - } - - @Override - public T setToNull(int i) { - return setValue(i, null); - } - - @Override - public T setToNull(String name) { - for (int i : getAllIndexesOf(name)) { - setToNull(i); - } - return wrapped; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof AbstractData)) return false; - - AbstractData that = (AbstractData) o; - if (values.length != that.values.length) return false; - - if (this.protocolVersion != that.protocolVersion) return false; - - // Deserializing each value is slightly inefficient, but comparing - // the bytes could in theory be wrong (for varint for instance, 2 values - // can have different binary representation but be the same value due to - // leading zeros). So we don't take any risk. - for (int i = 0; i < values.length; i++) { - DataType thisType = getType(i); - DataType thatType = that.getType(i); - if (!thisType.equals(thatType)) return false; - - Object thisValue = this.codecFor(i).deserialize(this.values[i], this.protocolVersion); - Object thatValue = that.codecFor(i).deserialize(that.values[i], that.protocolVersion); - if (!MoreObjects.equal(thisValue, thatValue)) return false; - } - return true; - } - - @Override - public int hashCode() { - // Same as equals - int hash = 31; - for (int i = 0; i < values.length; i++) - hash += - values[i] == null ? 1 : codecFor(i).deserialize(values[i], protocolVersion).hashCode(); - return hash; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/AbstractGettableByIndexData.java b/driver-core/src/main/java/com/datastax/driver/core/AbstractGettableByIndexData.java deleted file mode 100644 index 9d46e7c09f5..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AbstractGettableByIndexData.java +++ /dev/null @@ -1,326 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.InvalidTypeException; -import com.google.common.reflect.TypeToken; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -abstract class AbstractGettableByIndexData implements GettableByIndexData { - - protected final ProtocolVersion protocolVersion; - - protected AbstractGettableByIndexData(ProtocolVersion protocolVersion) { - this.protocolVersion = protocolVersion; - } - - /** - * Returns the type for the value at index {@code i}. - * - * @param i the index of the type to fetch. - * @return the type of the value at index {@code i}. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index. - */ - protected abstract DataType getType(int i); - - /** - * Returns the name corresponding to the value at index {@code i}. - * - * @param i the index of the name to fetch. - * @return the name corresponding to the value at index {@code i}. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index. - */ - protected abstract String getName(int i); - - /** - * Returns the value at index {@code i}. - * - * @param i the index to fetch. - * @return the value at index {@code i}. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index. - */ - protected abstract ByteBuffer getValue(int i); - - protected abstract CodecRegistry getCodecRegistry(); - - protected TypeCodec codecFor(int i) { - return getCodecRegistry().codecFor(getType(i)); - } - - protected TypeCodec codecFor(int i, Class javaClass) { - return getCodecRegistry().codecFor(getType(i), javaClass); - } - - protected TypeCodec codecFor(int i, TypeToken javaType) { - return getCodecRegistry().codecFor(getType(i), javaType); - } - - protected TypeCodec codecFor(int i, T value) { - return getCodecRegistry().codecFor(getType(i), value); - } - - protected void checkType(int i, DataType.Name actual) { - DataType.Name expected = getType(i).getName(); - if (!actual.isCompatibleWith(expected)) - throw new InvalidTypeException( - String.format("Value %s is of type %s, not %s", getName(i), expected, actual)); - } - - /** {@inheritDoc} */ - @Override - public boolean isNull(int i) { - return getValue(i) == null; - } - - /** {@inheritDoc} */ - @Override - public boolean getBool(int i) { - ByteBuffer value = getValue(i); - TypeCodec codec = codecFor(i, Boolean.class); - if (codec instanceof TypeCodec.PrimitiveBooleanCodec) - return ((TypeCodec.PrimitiveBooleanCodec) codec).deserializeNoBoxing(value, protocolVersion); - else return codec.deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public byte getByte(int i) { - ByteBuffer value = getValue(i); - TypeCodec codec = codecFor(i, Byte.class); - if (codec instanceof TypeCodec.PrimitiveByteCodec) - return ((TypeCodec.PrimitiveByteCodec) codec).deserializeNoBoxing(value, protocolVersion); - else return codec.deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public short getShort(int i) { - ByteBuffer value = getValue(i); - TypeCodec codec = codecFor(i, Short.class); - if (codec instanceof TypeCodec.PrimitiveShortCodec) - return ((TypeCodec.PrimitiveShortCodec) codec).deserializeNoBoxing(value, protocolVersion); - else return codec.deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public int getInt(int i) { - ByteBuffer value = getValue(i); - TypeCodec codec = codecFor(i, Integer.class); - if (codec instanceof TypeCodec.PrimitiveIntCodec) - return ((TypeCodec.PrimitiveIntCodec) codec).deserializeNoBoxing(value, protocolVersion); - else return codec.deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public long getLong(int i) { - ByteBuffer value = getValue(i); - TypeCodec codec = codecFor(i, Long.class); - if (codec instanceof TypeCodec.PrimitiveLongCodec) - return ((TypeCodec.PrimitiveLongCodec) codec).deserializeNoBoxing(value, protocolVersion); - else return codec.deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public Date getTimestamp(int i) { - ByteBuffer value = getValue(i); - return codecFor(i, Date.class).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public LocalDate getDate(int i) { - ByteBuffer value = getValue(i); - return codecFor(i, LocalDate.class).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public long getTime(int i) { - ByteBuffer value = getValue(i); - TypeCodec codec = codecFor(i, Long.class); - if (codec instanceof TypeCodec.PrimitiveLongCodec) - return ((TypeCodec.PrimitiveLongCodec) codec).deserializeNoBoxing(value, protocolVersion); - else return codec.deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public float getFloat(int i) { - ByteBuffer value = getValue(i); - TypeCodec codec = codecFor(i, Float.class); - if (codec instanceof TypeCodec.PrimitiveFloatCodec) - return ((TypeCodec.PrimitiveFloatCodec) codec).deserializeNoBoxing(value, protocolVersion); - else return codec.deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public double getDouble(int i) { - ByteBuffer value = getValue(i); - TypeCodec codec = codecFor(i, Double.class); - if (codec instanceof TypeCodec.PrimitiveDoubleCodec) - return ((TypeCodec.PrimitiveDoubleCodec) codec).deserializeNoBoxing(value, protocolVersion); - else return codec.deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public ByteBuffer getBytesUnsafe(int i) { - ByteBuffer value = getValue(i); - if (value == null) return null; - return value.duplicate(); - } - - /** {@inheritDoc} */ - @Override - public ByteBuffer getBytes(int i) { - ByteBuffer value = getValue(i); - return codecFor(i, ByteBuffer.class).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public String getString(int i) { - ByteBuffer value = getValue(i); - return codecFor(i, String.class).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public BigInteger getVarint(int i) { - ByteBuffer value = getValue(i); - return codecFor(i, BigInteger.class).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public BigDecimal getDecimal(int i) { - ByteBuffer value = getValue(i); - return codecFor(i, BigDecimal.class).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public UUID getUUID(int i) { - ByteBuffer value = getValue(i); - return codecFor(i, UUID.class).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public InetAddress getInet(int i) { - ByteBuffer value = getValue(i); - return codecFor(i, InetAddress.class).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("unchecked") - public List getList(int i, Class elementsClass) { - return getList(i, TypeToken.of(elementsClass)); - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("unchecked") - public List getList(int i, TypeToken elementsType) { - ByteBuffer value = getValue(i); - TypeToken> javaType = TypeTokens.listOf(elementsType); - return codecFor(i, javaType).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("unchecked") - public Set getSet(int i, Class elementsClass) { - return getSet(i, TypeToken.of(elementsClass)); - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("unchecked") - public Set getSet(int i, TypeToken elementsType) { - ByteBuffer value = getValue(i); - TypeToken> javaType = TypeTokens.setOf(elementsType); - return codecFor(i, javaType).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("unchecked") - public Map getMap(int i, Class keysClass, Class valuesClass) { - return getMap(i, TypeToken.of(keysClass), TypeToken.of(valuesClass)); - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("unchecked") - public Map getMap(int i, TypeToken keysType, TypeToken valuesType) { - ByteBuffer value = getValue(i); - TypeToken> javaType = TypeTokens.mapOf(keysType, valuesType); - return codecFor(i, javaType).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("unchecked") - public UDTValue getUDTValue(int i) { - ByteBuffer value = getValue(i); - return codecFor(i, UDTValue.class).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - @SuppressWarnings("unchecked") - public TupleValue getTupleValue(int i) { - ByteBuffer value = getValue(i); - return codecFor(i, TupleValue.class).deserialize(value, protocolVersion); - } - - /** {@inheritDoc} */ - @Override - public Object getObject(int i) { - return get(i, codecFor(i)); - } - - @Override - public T get(int i, Class targetClass) { - return get(i, codecFor(i, targetClass)); - } - - @Override - public T get(int i, TypeToken targetType) { - return get(i, codecFor(i, targetType)); - } - - @Override - public T get(int i, TypeCodec codec) { - checkType(i, codec.getCqlType().getName()); - ByteBuffer value = getValue(i); - return codec.deserialize(value, protocolVersion); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/AbstractGettableData.java b/driver-core/src/main/java/com/datastax/driver/core/AbstractGettableData.java deleted file mode 100644 index ec29ed7a17d..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AbstractGettableData.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.reflect.TypeToken; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -public abstract class AbstractGettableData extends AbstractGettableByIndexData - implements GettableData { - - /** - * Creates a new AbstractGettableData object. - * - * @param protocolVersion the protocol version in which values returned by {@link #getValue} will - * be returned. This must be a protocol version supported by this driver. In general, the - * correct value will be the value returned by {@link ProtocolOptions#getProtocolVersion}. - * @throws IllegalArgumentException if {@code protocolVersion} is not a valid protocol version. - */ - protected AbstractGettableData(ProtocolVersion protocolVersion) { - super(protocolVersion); - } - - /** - * Returns the index corresponding to a given name. - * - * @param name the name for which to return the index of. - * @return the index for the value coressponding to {@code name}. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - */ - protected abstract int getIndexOf(String name); - - /** {@inheritDoc} */ - @Override - public boolean isNull(String name) { - return isNull(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public boolean getBool(String name) { - return getBool(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public byte getByte(String name) { - return getByte(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public short getShort(String name) { - return getShort(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public int getInt(String name) { - return getInt(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public long getLong(String name) { - return getLong(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public Date getTimestamp(String name) { - return getTimestamp(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public LocalDate getDate(String name) { - return getDate(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public long getTime(String name) { - return getTime(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public float getFloat(String name) { - return getFloat(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public double getDouble(String name) { - return getDouble(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public ByteBuffer getBytesUnsafe(String name) { - return getBytesUnsafe(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public ByteBuffer getBytes(String name) { - return getBytes(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public String getString(String name) { - return getString(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public BigInteger getVarint(String name) { - return getVarint(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public BigDecimal getDecimal(String name) { - return getDecimal(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public UUID getUUID(String name) { - return getUUID(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public InetAddress getInet(String name) { - return getInet(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public List getList(String name, Class elementsClass) { - return getList(getIndexOf(name), elementsClass); - } - - /** {@inheritDoc} */ - @Override - public List getList(String name, TypeToken elementsType) { - return getList(getIndexOf(name), elementsType); - } - - /** {@inheritDoc} */ - @Override - public Set getSet(String name, Class elementsClass) { - return getSet(getIndexOf(name), elementsClass); - } - - /** {@inheritDoc} */ - @Override - public Set getSet(String name, TypeToken elementsType) { - return getSet(getIndexOf(name), elementsType); - } - - /** {@inheritDoc} */ - @Override - public Map getMap(String name, Class keysClass, Class valuesClass) { - return getMap(getIndexOf(name), keysClass, valuesClass); - } - - /** {@inheritDoc} */ - @Override - public Map getMap(String name, TypeToken keysType, TypeToken valuesType) { - return getMap(getIndexOf(name), keysType, valuesType); - } - - /** {@inheritDoc} */ - @Override - public UDTValue getUDTValue(String name) { - return getUDTValue(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public TupleValue getTupleValue(String name) { - return getTupleValue(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public Object getObject(String name) { - return getObject(getIndexOf(name)); - } - - /** {@inheritDoc} */ - @Override - public T get(String name, Class targetClass) { - return get(getIndexOf(name), targetClass); - } - - /** {@inheritDoc} */ - @Override - public T get(String name, TypeToken targetType) { - return get(getIndexOf(name), targetType); - } - - /** {@inheritDoc} */ - @Override - public T get(String name, TypeCodec codec) { - return get(getIndexOf(name), codec); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/AbstractMonotonicTimestampGenerator.java b/driver-core/src/main/java/com/datastax/driver/core/AbstractMonotonicTimestampGenerator.java deleted file mode 100644 index 4c84113526c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AbstractMonotonicTimestampGenerator.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.annotations.VisibleForTesting; - -/** - * Base implementation for monotonic timestamp generators. - * - *

The accuracy of the generated timestamps is largely dependent on the granularity of the - * underlying operating system's clock. - * - *

Generally speaking, this granularity is millisecond, and the sub-millisecond part is simply a - * counter that gets incremented until the next clock tick, as provided by {@link - * System#currentTimeMillis()}. - * - *

On some systems, however, it is possible to have a better granularity by using a JNR call to - * {@code gettimeofday}. The driver will use this system call automatically whenever available, - * unless the system property {@code com.datastax.driver.USE_NATIVE_CLOCK} is explicitly set to - * {@code false}. - * - *

Beware that to guarantee monotonicity, if more than one call to {@link #next()} is made within - * the same microsecond, or in the event of a system clock skew, this generator might return - * timestamps that drift out in the future. Whe this happens, {@link #onDrift(long, long)} is - * invoked. - */ -public abstract class AbstractMonotonicTimestampGenerator implements TimestampGenerator { - - @VisibleForTesting volatile Clock clock = ClockFactory.newInstance(); - - /** - * Compute the next timestamp, given the last timestamp previously generated. - * - *

To guarantee monotonicity, the next timestamp should be strictly greater than the last one. - * If the underlying clock fails to generate monotonically increasing timestamps, the generator - * will simply increment the previous timestamp, and {@link #onDrift(long, long)} will be invoked. - * - *

This implementation is inspired by {@code - * org.apache.cassandra.service.ClientState#getTimestamp()}. - * - * @param last the last timestamp generated by this generator, in microseconds. - * @return the next timestamp to use, in microseconds. - */ - protected long computeNext(long last) { - long currentTick = clock.currentTimeMicros(); - if (last >= currentTick) { - onDrift(currentTick, last); - return last + 1; - } - return currentTick; - } - - /** - * Called when generated timestamps drift into the future compared to the underlying clock (in - * other words, if {@code lastTimestamp >= currentTick}). - * - *

This could happen if timestamps are requested faster than the clock granularity, or on a - * clock skew (for example because of a leap second). - * - * @param currentTick the current clock tick, in microseconds. - * @param lastTimestamp the last timestamp that was generated, in microseconds. - */ - protected abstract void onDrift(long currentTick, long lastTimestamp); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/AbstractReconnectionHandler.java b/driver-core/src/main/java/com/datastax/driver/core/AbstractReconnectionHandler.java deleted file mode 100644 index 97865150468..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AbstractReconnectionHandler.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.AuthenticationException; -import com.datastax.driver.core.exceptions.ConnectionException; -import com.datastax.driver.core.exceptions.UnsupportedProtocolVersionException; -import com.datastax.driver.core.policies.ReconnectionPolicy; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.util.concurrent.AbstractFuture; -import com.google.common.util.concurrent.ListenableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Manages periodic reconnection attempts after a host has been marked down. - * - *

Concurrent attempts are handled via the {@link #currentAttempt} reference passed to the - * constructor. For a given reference, only one handler will run at a given time. Additional - * handlers will cancel themselves if they find a previous handler running. - * - *

This class is designed for concurrency, but instances must not be shared: each thread creates - * and starts its own private handler, all interactions happen through {@link #currentAttempt}. - */ -abstract class AbstractReconnectionHandler implements Runnable { - - private static final Logger logger = LoggerFactory.getLogger(AbstractReconnectionHandler.class); - - private final String name; - private final ScheduledExecutorService executor; - private final ReconnectionPolicy.ReconnectionSchedule schedule; - /** - * The future that is exposed to clients, representing completion of the current active handler - */ - private final AtomicReference> currentAttempt; - - @VisibleForTesting final HandlerFuture handlerFuture = new HandlerFuture(); - - private final long initialDelayMs; - - private final CountDownLatch ready = new CountDownLatch(1); - - public AbstractReconnectionHandler( - String name, - ScheduledExecutorService executor, - ReconnectionPolicy.ReconnectionSchedule schedule, - AtomicReference> currentAttempt) { - this(name, executor, schedule, currentAttempt, -1); - } - - public AbstractReconnectionHandler( - String name, - ScheduledExecutorService executor, - ReconnectionPolicy.ReconnectionSchedule schedule, - AtomicReference> currentAttempt, - long initialDelayMs) { - this.name = name; - this.executor = executor; - this.schedule = schedule; - this.currentAttempt = currentAttempt; - this.initialDelayMs = initialDelayMs; - } - - protected abstract Connection tryReconnect() - throws ConnectionException, InterruptedException, UnsupportedProtocolVersionException, - ClusterNameMismatchException; - - protected abstract void onReconnection(Connection connection); - - protected boolean onConnectionException(ConnectionException e, long nextDelayMs) { - return true; - } - - protected boolean onUnknownException(Exception e, long nextDelayMs) { - return true; - } - - // Retrying on authentication errors makes sense for applications that can update the credentials - // at runtime, we don't want to force them - // to restart. - protected boolean onAuthenticationException(AuthenticationException e, long nextDelayMs) { - return true; - } - - // Retrying on these errors is unlikely to work - protected boolean onUnsupportedProtocolVersionException( - UnsupportedProtocolVersionException e, long nextDelayMs) { - return false; - } - - protected boolean onClusterNameMismatchException( - ClusterNameMismatchException e, long nextDelayMs) { - return false; - } - - public void start() { - long firstDelay = (initialDelayMs >= 0) ? initialDelayMs : schedule.nextDelayMs(); - logger.debug("First reconnection scheduled in {}ms", firstDelay); - try { - handlerFuture.nextTry = executor.schedule(this, firstDelay, TimeUnit.MILLISECONDS); - - while (true) { - ListenableFuture previous = currentAttempt.get(); - if (previous != null && !previous.isCancelled()) { - logger.debug("Found another already active handler, cancelling"); - handlerFuture.cancel(false); - break; - } - if (currentAttempt.compareAndSet(previous, handlerFuture)) { - Host.statesLogger.debug("[{}] starting reconnection attempt", name); - break; - } - } - ready.countDown(); - } catch (RejectedExecutionException e) { - // The executor has been shutdown, fair enough, just ignore - logger.debug("Aborting reconnection handling since the cluster is shutting down"); - } - } - - @Override - public void run() { - // Just make sure we don't start the first try too fast, in case we find out in start() that we - // need to cancel ourselves - try { - ready.await(); - } catch (InterruptedException e) { - // This can happen at shutdown - Thread.currentThread().interrupt(); - return; - } - - if (handlerFuture.isCancelled()) { - logger.debug("Got cancelled, stopping"); - return; - } - - try { - onReconnection(tryReconnect()); - handlerFuture.markAsDone(); - currentAttempt.compareAndSet(handlerFuture, null); - logger.debug("Reconnection successful, cleared the future"); - } catch (ConnectionException e) { - long nextDelay = schedule.nextDelayMs(); - if (onConnectionException(e, nextDelay)) reschedule(nextDelay); - else currentAttempt.compareAndSet(handlerFuture, null); - } catch (AuthenticationException e) { - logger.error(e.getMessage()); - long nextDelay = schedule.nextDelayMs(); - if (onAuthenticationException(e, nextDelay)) { - reschedule(nextDelay); - } else { - logger.error( - "Retries against {} have been suspended. It won't be retried unless the node is restarted.", - e.getEndPoint()); - currentAttempt.compareAndSet(handlerFuture, null); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (UnsupportedProtocolVersionException e) { - logger.error(e.getMessage()); - long nextDelay = schedule.nextDelayMs(); - if (onUnsupportedProtocolVersionException(e, nextDelay)) { - reschedule(nextDelay); - } else { - logger.error( - "Retries against {} have been suspended. It won't be retried unless the node is restarted.", - e.getEndPoint()); - currentAttempt.compareAndSet(handlerFuture, null); - } - } catch (ClusterNameMismatchException e) { - logger.error(e.getMessage()); - long nextDelay = schedule.nextDelayMs(); - if (onClusterNameMismatchException(e, nextDelay)) { - reschedule(nextDelay); - } else { - logger.error( - "Retries against {} have been suspended. It won't be retried unless the node is restarted.", - e.endPoint); - currentAttempt.compareAndSet(handlerFuture, null); - } - } catch (Exception e) { - long nextDelay = schedule.nextDelayMs(); - if (onUnknownException(e, nextDelay)) reschedule(nextDelay); - else currentAttempt.compareAndSet(handlerFuture, null); - } - } - - private void reschedule(long nextDelay) { - // If we got cancelled during the failed reconnection attempt that lead here, don't reschedule - if (handlerFuture.isCancelled()) { - currentAttempt.compareAndSet(handlerFuture, null); - return; - } - - Host.statesLogger.debug("[{}] next reconnection attempt in {} ms", name, nextDelay); - handlerFuture.nextTry = executor.schedule(this, nextDelay, TimeUnit.MILLISECONDS); - } - - // The future that the handler exposes to its clients via currentAttempt - @VisibleForTesting - static class HandlerFuture extends AbstractFuture { - // A future representing completion of the next task submitted to the executor - volatile ScheduledFuture nextTry; - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - // This is a check-then-act, so we may race with the scheduling of the first try, but in that - // case - // we'll re-check for cancellation when this first try starts running - if (nextTry != null) { - nextTry.cancel(mayInterruptIfRunning); - } - - return super.cancel(mayInterruptIfRunning); - } - - void markAsDone() { - super.set(null); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/AbstractSession.java b/driver-core/src/main/java/com/datastax/driver/core/AbstractSession.java deleted file mode 100644 index 61de3414fd2..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AbstractSession.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.base.Function; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.Uninterruptibles; -import io.netty.util.concurrent.EventExecutor; -import java.nio.ByteBuffer; -import java.util.Map; -import java.util.concurrent.ExecutionException; - -/** - * Abstract implementation of the Session interface. - * - *

This is primarly intended to make mocking easier. - */ -public abstract class AbstractSession implements Session { - - private static final boolean CHECK_IO_DEADLOCKS = - SystemProperties.getBoolean("com.datastax.driver.CHECK_IO_DEADLOCKS", true); - - /** {@inheritDoc} */ - @Override - public ResultSet execute(String query) { - return execute(new SimpleStatement(query)); - } - - /** {@inheritDoc} */ - @Override - public ResultSet execute(String query, Object... values) { - return execute(new SimpleStatement(query, values)); - } - - /** {@inheritDoc} */ - @Override - public ResultSet execute(String query, Map values) { - return execute(new SimpleStatement(query, values)); - } - - /** {@inheritDoc} */ - @Override - public ResultSet execute(Statement statement) { - checkNotInEventLoop(); - return executeAsync(statement).getUninterruptibly(); - } - - /** {@inheritDoc} */ - @Override - public ResultSetFuture executeAsync(String query) { - return executeAsync(new SimpleStatement(query)); - } - - /** {@inheritDoc} */ - @Override - public ResultSetFuture executeAsync(String query, Map values) { - return executeAsync(new SimpleStatement(query, values)); - } - - /** {@inheritDoc} */ - @Override - public ResultSetFuture executeAsync(String query, Object... values) { - return executeAsync(new SimpleStatement(query, values)); - } - - /** {@inheritDoc} */ - @Override - public PreparedStatement prepare(String query) { - checkNotInEventLoop(); - try { - return Uninterruptibles.getUninterruptibly(prepareAsync(query)); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } - - /** {@inheritDoc} */ - @Override - public PreparedStatement prepare(RegularStatement statement) { - checkNotInEventLoop(); - try { - return Uninterruptibles.getUninterruptibly(prepareAsync(statement)); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } - - /** {@inheritDoc} */ - @Override - public ListenableFuture prepareAsync(String query) { - return prepareAsync(query, null); - } - - /** {@inheritDoc} */ - @Override - public ListenableFuture prepareAsync(final RegularStatement statement) { - - if (statement.hasValues()) - throw new IllegalArgumentException("A statement to prepare should not have values"); - - final CodecRegistry codecRegistry = getCluster().getConfiguration().getCodecRegistry(); - ListenableFuture prepared = - prepareAsync(statement.getQueryString(codecRegistry), statement.getOutgoingPayload()); - return GuavaCompatibility.INSTANCE.transform( - prepared, - new Function() { - @Override - public PreparedStatement apply(PreparedStatement prepared) { - ProtocolVersion protocolVersion = - getCluster().getConfiguration().getProtocolOptions().getProtocolVersion(); - ByteBuffer routingKey = statement.getRoutingKey(protocolVersion, codecRegistry); - if (routingKey != null) prepared.setRoutingKey(routingKey); - if (statement.getConsistencyLevel() != null) - prepared.setConsistencyLevel(statement.getConsistencyLevel()); - if (statement.getSerialConsistencyLevel() != null) - prepared.setSerialConsistencyLevel(statement.getSerialConsistencyLevel()); - if (statement.isTracing()) prepared.enableTracing(); - prepared.setRetryPolicy(statement.getRetryPolicy()); - prepared.setOutgoingPayload(statement.getOutgoingPayload()); - prepared.setIdempotent(statement.isIdempotent()); - - return prepared; - } - }); - } - - /** - * Prepares the provided query string asynchronously, sending along the provided custom payload, - * if any. - * - * @param query the CQL query string to prepare - * @param customPayload the custom payload to send along the query, or {@code null} if no payload - * is to be sent - * @return a future on the prepared statement corresponding to {@code query}. - */ - protected abstract ListenableFuture prepareAsync( - String query, Map customPayload); - - /** {@inheritDoc} */ - @Override - public void close() { - try { - closeAsync().get(); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - /** - * Checks that the current thread is not one of the Netty I/O threads used by the driver. - * - *

This method is called from all the synchronous methods of this class to prevent deadlock - * issues. - * - *

User code extending this class can also call this method at any time to check if any code - * making blocking calls is being wrongly executed on a Netty I/O thread. - * - *

Note that the check performed by this method has a small overhead; if that is an issue, - * checks can be disabled by setting the System property {@code - * com.datastax.driver.CHECK_IO_DEADLOCKS} to {@code false}. - * - * @throws IllegalStateException if the current thread is one of the Netty I/O thread used by the - * driver. - */ - public void checkNotInEventLoop() { - Connection.Factory connectionFactory = getCluster().manager.connectionFactory; - if (!CHECK_IO_DEADLOCKS || connectionFactory == null) return; - for (EventExecutor executor : connectionFactory.eventLoopGroup) { - if (executor.inEventLoop()) { - throw new IllegalStateException( - "Detected a synchronous call on an I/O thread, this can cause deadlocks or unpredictable " - + "behavior. This generally happens when a Future callback calls a synchronous Session " - + "method (execute() or prepare()), or iterates a result set past the fetch size " - + "(causing an internal synchronous fetch of the next page of results). " - + "Avoid this in your callbacks, or schedule them on a different executor."); - } - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/AbstractTableMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/AbstractTableMetadata.java deleted file mode 100644 index 46c86c11d28..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AbstractTableMetadata.java +++ /dev/null @@ -1,376 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.base.Predicate; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -/** Base class for Tables and Materialized Views metadata. */ -public abstract class AbstractTableMetadata { - - static final Comparator columnMetadataComparator = - new Comparator() { - @Override - public int compare(ColumnMetadata c1, ColumnMetadata c2) { - return c1.getName().compareTo(c2.getName()); - } - }; - - // comparator for ordering tables and views by name. - static final Comparator byNameComparator = - new Comparator() { - @Override - public int compare(AbstractTableMetadata o1, AbstractTableMetadata o2) { - return o1.getName().compareTo(o2.getName()); - } - }; - - static final Predicate isAscending = - new Predicate() { - @Override - public boolean apply(ClusteringOrder o) { - return o == ClusteringOrder.ASC; - } - }; - - protected final KeyspaceMetadata keyspace; - protected final String name; - protected final UUID id; - protected final List partitionKey; - protected final List clusteringColumns; - protected final Map columns; - protected final TableOptionsMetadata options; - protected final List clusteringOrder; - protected final VersionNumber cassandraVersion; - - protected AbstractTableMetadata( - KeyspaceMetadata keyspace, - String name, - UUID id, - List partitionKey, - List clusteringColumns, - Map columns, - TableOptionsMetadata options, - List clusteringOrder, - VersionNumber cassandraVersion) { - this.keyspace = keyspace; - this.name = name; - this.id = id; - this.partitionKey = partitionKey; - this.clusteringColumns = clusteringColumns; - this.columns = columns; - this.options = options; - this.clusteringOrder = clusteringOrder; - this.cassandraVersion = cassandraVersion; - } - - /** - * Returns the name of this table. - * - * @return the name of this CQL table. - */ - public String getName() { - return name; - } - - /** - * Returns the unique id of this table. - * - *

Note: this id is available in Cassandra 2.1 and above. It will be {@code null} for earlier - * versions. - * - * @return the unique id of the table. - */ - public UUID getId() { - return id; - } - - /** - * Returns the keyspace this table belong to. - * - * @return the keyspace metadata of the keyspace this table belong to. - */ - public KeyspaceMetadata getKeyspace() { - return keyspace; - } - - /** - * Returns metadata on a column of this table. - * - * @param name the name of the column to retrieve ({@code name} will be interpreted as a - * case-insensitive identifier unless enclosed in double-quotes, see {@link Metadata#quote}). - * @return the metadata for the column if it exists, or {@code null} otherwise. - */ - public ColumnMetadata getColumn(String name) { - return columns.get(Metadata.handleId(name)); - } - - /** - * Returns a list containing all the columns of this table. - * - *

The order of the columns in the list is consistent with the order of the columns returned by - * a {@code SELECT * FROM thisTable}: the first column is the partition key, next are the - * clustering columns in their defined order, and then the rest of the columns follow in - * alphabetic order. - * - * @return a list containing the metadata for the columns of this table. - */ - public List getColumns() { - return new ArrayList(columns.values()); - } - - /** - * Returns the list of columns composing the primary key for this table. - * - *

A table will always at least have a partition key (that may itself be one or more columns), - * so the returned list at least has one element. - * - * @return the list of columns composing the primary key for this table. - */ - public List getPrimaryKey() { - List pk = - new ArrayList(partitionKey.size() + clusteringColumns.size()); - pk.addAll(partitionKey); - pk.addAll(clusteringColumns); - return pk; - } - - /** - * Returns the list of columns composing the partition key for this table. - * - *

A table always has a partition key so the returned list has at least one element. - * - * @return the list of columns composing the partition key for this table. - */ - public List getPartitionKey() { - return Collections.unmodifiableList(partitionKey); - } - - /** - * Returns the list of clustering columns for this table. - * - * @return the list of clustering columns for this table. If there is no clustering columns, an - * empty list is returned. - */ - public List getClusteringColumns() { - return Collections.unmodifiableList(clusteringColumns); - } - - /** - * Returns the clustering order for this table. - * - *

The returned contains the clustering order of each clustering column. The {@code i}th - * element of the result correspond to the order (ascending or descending) of the {@code i}th - * clustering column (see {@link #getClusteringColumns}). Note that a table defined without any - * particular clustering order is equivalent to one for which all the clustering keys are in - * ascending order. - * - * @return a list with the clustering order for each clustering column. - */ - public List getClusteringOrder() { - return clusteringOrder; - } - - /** - * Returns the options for this table. - * - *

This value will be null for virtual tables. - * - * @return the options for this table. - */ - public TableOptionsMetadata getOptions() { - return options; - } - - /** - * Returns whether or not this table is a virtual table - * - * @return {@code true} if virtual keyspace, {@code false} otherwise. - */ - public boolean isVirtual() { - return getKeyspace().isVirtual(); - } - - void add(ColumnMetadata column) { - columns.put(column.getName(), column); - } - - /** - * Returns a {@code String} containing CQL queries representing this table and the index on it. - * - *

In other words, this method returns the queries that would allow you to recreate the schema - * of this table, along with the indexes and views defined on this table, if any. - * - *

Note that the returned String is formatted to be human readable (for some definition of - * human readable at least). - * - * @return the CQL queries representing this table schema as a {code String}. - */ - public String exportAsString() { - StringBuilder sb = new StringBuilder(); - - sb.append(asCQLQuery(true)); - - return sb.toString(); - } - - /** - * Returns a CQL query representing this table. - * - *

This method returns a single 'CREATE TABLE' query with the options corresponding to this - * table definition. - * - *

Note that the returned string is a single line; the returned query is not formatted in any - * way. - * - * @return the 'CREATE TABLE' query corresponding to this table. - * @see #exportAsString - */ - public String asCQLQuery() { - return asCQLQuery(false); - } - - protected abstract String asCQLQuery(boolean formatted); - - protected StringBuilder appendOptions(StringBuilder sb, boolean formatted) { - // Options - if (options == null) { - return sb; - } - sb.append("WITH "); - if (options.isCompactStorage()) and(sb.append("COMPACT STORAGE"), formatted); - if (!clusteringOrder.isEmpty()) and(appendClusteringOrder(sb), formatted); - if (cassandraVersion.getMajor() < 4) - sb.append("read_repair_chance = ").append(options.getReadRepairChance()); - else sb.append("read_repair = '").append(options.getReadRepair()).append('\''); - if (cassandraVersion.getMajor() < 4) - and(sb, formatted) - .append("dclocal_read_repair_chance = ") - .append(options.getLocalReadRepairChance()); - if (cassandraVersion.getMajor() < 2 - || (cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() == 0)) - and(sb, formatted).append("replicate_on_write = ").append(options.getReplicateOnWrite()); - and(sb, formatted).append("gc_grace_seconds = ").append(options.getGcGraceInSeconds()); - if (cassandraVersion.getMajor() > 3) - and(sb, formatted) - .append("additional_write_policy = '") - .append(options.getAdditionalWritePolicy()) - .append('\''); - and(sb, formatted) - .append("bloom_filter_fp_chance = ") - .append(options.getBloomFilterFalsePositiveChance()); - if (cassandraVersion.getMajor() < 2 - || cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() < 1) - and(sb, formatted) - .append("caching = '") - .append(options.getCaching().get("keys")) - .append('\''); - else and(sb, formatted).append("caching = ").append(formatOptionMap(options.getCaching())); - if (options.getComment() != null) - and(sb, formatted) - .append("comment = '") - .append(options.getComment().replace("'", "''")) - .append('\''); - and(sb, formatted).append("compaction = ").append(formatOptionMap(options.getCompaction())); - and(sb, formatted).append("compression = ").append(formatOptionMap(options.getCompression())); - if (cassandraVersion.getMajor() >= 2) { - and(sb, formatted).append("default_time_to_live = ").append(options.getDefaultTimeToLive()); - and(sb, formatted) - .append("speculative_retry = '") - .append(options.getSpeculativeRetry()) - .append('\''); - if (options.getIndexInterval() != null) - and(sb, formatted).append("index_interval = ").append(options.getIndexInterval()); - } - if (cassandraVersion.getMajor() > 2 - || (cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() >= 1)) { - and(sb, formatted).append("min_index_interval = ").append(options.getMinIndexInterval()); - and(sb, formatted).append("max_index_interval = ").append(options.getMaxIndexInterval()); - } - if (cassandraVersion.getMajor() > 2) { - and(sb, formatted).append("crc_check_chance = ").append(options.getCrcCheckChance()); - } - if (cassandraVersion.getMajor() > 3 - || (cassandraVersion.getMajor() == 3 && cassandraVersion.getMinor() >= 8)) { - and(sb, formatted).append("cdc = ").append(options.isCDC()); - } - if (cassandraVersion.getMajor() > 1) { - and(sb, formatted) - .append("memtable_flush_period_in_ms = ") - .append(options.getMemtableFlushPeriodInMs()); - } - sb.append(';'); - return sb; - } - - @Override - public String toString() { - if (keyspace.isVirtual()) { - return name; - } - - return asCQLQuery(); - } - - private StringBuilder appendClusteringOrder(StringBuilder sb) { - sb.append("CLUSTERING ORDER BY ("); - for (int i = 0; i < clusteringColumns.size(); i++) { - if (i > 0) sb.append(", "); - sb.append(Metadata.quoteIfNecessary(clusteringColumns.get(i).getName())) - .append(' ') - .append(clusteringOrder.get(i)); - } - return sb.append(')'); - } - - private static String formatOptionMap(Map m) { - StringBuilder sb = new StringBuilder(); - sb.append("{ "); - boolean first = true; - for (Map.Entry entry : m.entrySet()) { - if (first) first = false; - else sb.append(", "); - sb.append('\'').append(entry.getKey()).append('\''); - sb.append(" : "); - try { - sb.append(Integer.parseInt(entry.getValue())); - } catch (NumberFormatException e) { - sb.append('\'').append(entry.getValue()).append('\''); - } - } - sb.append(" }"); - return sb.toString(); - } - - private StringBuilder and(StringBuilder sb, boolean formatted) { - return spaceOrNewLine(sb, formatted).append("AND "); - } - - static StringBuilder newLine(StringBuilder sb, boolean formatted) { - if (formatted) sb.append('\n'); - return sb; - } - - static StringBuilder spaceOrNewLine(StringBuilder sb, boolean formatted) { - sb.append(formatted ? "\n " : ' '); - return sb; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/AggregateMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/AggregateMetadata.java deleted file mode 100644 index 65b149e7542..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AggregateMetadata.java +++ /dev/null @@ -1,440 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.Bytes; -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Describes a CQL aggregate function (created with {@code CREATE AGGREGATE...}). */ -public class AggregateMetadata { - - private static final Logger LOGGER = LoggerFactory.getLogger(AggregateMetadata.class); - - private final KeyspaceMetadata keyspace; - private final String simpleName; - private final List argumentTypes; - private final String finalFuncSimpleName; - private final String finalFuncFullName; - private final Object initCond; - private final DataType returnType; - private final String stateFuncSimpleName; - private final String stateFuncFullName; - private final DataType stateType; - private final TypeCodec stateTypeCodec; - - private AggregateMetadata( - KeyspaceMetadata keyspace, - String simpleName, - List argumentTypes, - String finalFuncSimpleName, - String finalFuncFullName, - Object initCond, - DataType returnType, - String stateFuncSimpleName, - String stateFuncFullName, - DataType stateType, - TypeCodec stateTypeCodec) { - this.keyspace = keyspace; - this.simpleName = simpleName; - this.argumentTypes = argumentTypes; - this.finalFuncSimpleName = finalFuncSimpleName; - this.finalFuncFullName = finalFuncFullName; - this.initCond = initCond; - this.returnType = returnType; - this.stateFuncSimpleName = stateFuncSimpleName; - this.stateFuncFullName = stateFuncFullName; - this.stateType = stateType; - this.stateTypeCodec = stateTypeCodec; - } - - // Cassandra < 3.0: - // CREATE TABLE system.schema_aggregates ( - // keyspace_name text, - // aggregate_name text, - // signature frozen>, - // argument_types list, - // final_func text, - // initcond blob, - // return_type text, - // state_func text, - // state_type text, - // PRIMARY KEY (keyspace_name, aggregate_name, signature) - // ) WITH CLUSTERING ORDER BY (aggregate_name ASC, signature ASC) - // - // Cassandra >= 3.0: - // CREATE TABLE system.schema_aggregates ( - // keyspace_name text, - // aggregate_name text, - // argument_types frozen>, - // final_func text, - // initcond text, - // return_type text, - // state_func text, - // state_type text, - // PRIMARY KEY (keyspace_name, aggregate_name, argument_types) - // ) WITH CLUSTERING ORDER BY (aggregate_name ASC, argument_types ASC) - static AggregateMetadata build( - KeyspaceMetadata ksm, Row row, VersionNumber version, Cluster cluster) { - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - String simpleName = row.getString("aggregate_name"); - List argumentTypes = - parseTypes(ksm, row.getList("argument_types", String.class), version, cluster); - String finalFuncSimpleName = row.getString("final_func"); - DataType returnType; - if (version.getMajor() >= 3) { - returnType = - DataTypeCqlNameParser.parse( - row.getString("return_type"), - cluster, - ksm.getName(), - ksm.userTypes, - null, - false, - false); - } else { - returnType = - DataTypeClassNameParser.parseOne( - row.getString("return_type"), protocolVersion, codecRegistry); - } - String stateFuncSimpleName = row.getString("state_func"); - String stateTypeName = row.getString("state_type"); - DataType stateType; - Object initCond; - if (version.getMajor() >= 3) { - stateType = - DataTypeCqlNameParser.parse( - stateTypeName, cluster, ksm.getName(), ksm.userTypes, null, false, false); - String rawInitCond = row.getString("initcond"); - if (rawInitCond == null) { - initCond = null; - } else { - try { - initCond = codecRegistry.codecFor(stateType).parse(rawInitCond); - } catch (RuntimeException e) { - LOGGER.warn( - "Failed to parse INITCOND literal: {}; getInitCond() will return the text literal instead.", - rawInitCond); - initCond = rawInitCond; - } - } - } else { - stateType = DataTypeClassNameParser.parseOne(stateTypeName, protocolVersion, codecRegistry); - ByteBuffer rawInitCond = row.getBytes("initcond"); - if (rawInitCond == null) { - initCond = null; - } else { - try { - initCond = codecRegistry.codecFor(stateType).deserialize(rawInitCond, protocolVersion); - } catch (RuntimeException e) { - LOGGER.warn( - "Failed to deserialize INITCOND value: {}; getInitCond() will return the raw bytes instead.", - Bytes.toHexString(rawInitCond)); - initCond = rawInitCond; - } - } - } - - String finalFuncFullName = - finalFuncSimpleName == null - ? null - : Metadata.fullFunctionName(finalFuncSimpleName, Collections.singletonList(stateType)); - String stateFuncFullName = makeStateFuncFullName(stateFuncSimpleName, stateType, argumentTypes); - - return new AggregateMetadata( - ksm, - simpleName, - argumentTypes, - finalFuncSimpleName, - finalFuncFullName, - initCond, - returnType, - stateFuncSimpleName, - stateFuncFullName, - stateType, - codecRegistry.codecFor(stateType)); - } - - private static String makeStateFuncFullName( - String stateFuncSimpleName, DataType stateType, List argumentTypes) { - List args = Lists.newArrayList(stateType); - args.addAll(argumentTypes); - return Metadata.fullFunctionName(stateFuncSimpleName, args); - } - - private static List parseTypes( - KeyspaceMetadata ksm, List types, VersionNumber version, Cluster cluster) { - if (types.isEmpty()) return Collections.emptyList(); - - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - ImmutableList.Builder builder = ImmutableList.builder(); - for (String name : types) { - DataType type; - if (version.getMajor() >= 3) { - type = - DataTypeCqlNameParser.parse( - name, cluster, ksm.getName(), ksm.userTypes, null, false, false); - } else { - type = DataTypeClassNameParser.parseOne(name, protocolVersion, codecRegistry); - } - builder.add(type); - } - return builder.build(); - } - - /** - * Returns a CQL query representing this function in human readable form. - * - *

This method is equivalent to {@link #asCQLQuery} but the output is formatted. - * - * @return the CQL query representing this function. - */ - public String exportAsString() { - return asCQLQuery(true); - } - - /** - * Returns a CQL query representing this function. - * - *

This method returns a single 'CREATE FUNCTION' query corresponding to this function - * definition. - * - * @return the 'CREATE FUNCTION' query corresponding to this function. - */ - public String asCQLQuery() { - return asCQLQuery(false); - } - - @Override - public String toString() { - return asCQLQuery(false); - } - - private String asCQLQuery(boolean formatted) { - - StringBuilder sb = - new StringBuilder("CREATE AGGREGATE ") - .append(Metadata.quoteIfNecessary(keyspace.getName())) - .append('.'); - - appendSignature(sb); - - TableMetadata.spaceOrNewLine(sb, formatted) - .append("SFUNC ") - .append(Metadata.quoteIfNecessary(stateFuncSimpleName)); - TableMetadata.spaceOrNewLine(sb, formatted) - .append("STYPE ") - .append(stateType.asFunctionParameterString()); - - if (finalFuncSimpleName != null) - TableMetadata.spaceOrNewLine(sb, formatted) - .append("FINALFUNC ") - .append(Metadata.quoteIfNecessary(finalFuncSimpleName)); - - if (initCond != null) - TableMetadata.spaceOrNewLine(sb, formatted).append("INITCOND ").append(formatInitCond()); - - sb.append(';'); - - return sb.toString(); - } - - private String formatInitCond() { - if (stateTypeCodec.accepts(initCond)) { - try { - return stateTypeCodec.format(initCond); - } catch (RuntimeException e) { - LOGGER.info("Failed to format INITCOND literal: {}", initCond); - } - } - return initCond.toString(); - } - - private void appendSignature(StringBuilder sb) { - sb.append(Metadata.quoteIfNecessary(simpleName)).append('('); - boolean first = true; - for (DataType type : argumentTypes) { - if (first) first = false; - else sb.append(','); - sb.append(type.asFunctionParameterString()); - } - sb.append(')'); - } - - /** - * Returns the keyspace this aggregate belongs to. - * - * @return the keyspace metadata of the keyspace this aggregate belongs to. - */ - public KeyspaceMetadata getKeyspace() { - return keyspace; - } - - /** - * Returns the CQL signature of this aggregate. - * - *

This is the name of the aggregate, followed by the names of the argument types between - * parentheses, like it was specified in the {@code CREATE AGGREGATE...} statement, for example - * {@code sum(int)}. - * - *

Note that the returned signature is not qualified with the keyspace name. - * - * @return the signature of this aggregate. - */ - public String getSignature() { - StringBuilder sb = new StringBuilder(); - appendSignature(sb); - return sb.toString(); - } - - /** - * Returns the simple name of this aggregate. - * - *

This is the name of the aggregate, without arguments. Note that aggregates can be overloaded - * with different argument lists, therefore the simple name may not be unique. For example, {@code - * sum(int)} and {@code sum(int,int)} both have the simple name {@code sum}. - * - * @return the simple name of this aggregate. - * @see #getSignature() - */ - public String getSimpleName() { - return simpleName; - } - - /** - * Returns the types of this aggregate's arguments. - * - * @return the types. - */ - public List getArgumentTypes() { - return argumentTypes; - } - - /** - * Returns the final function of this aggregate. - * - *

This is the function specified with {@code FINALFUNC} in the {@code CREATE AGGREGATE...} - * statement. It transforms the final value after the aggregation is complete. - * - * @return the metadata of the final function, or {@code null} if there is none. - */ - public FunctionMetadata getFinalFunc() { - return (finalFuncFullName == null) ? null : keyspace.functions.get(finalFuncFullName); - } - - /** - * Returns the initial state value of this aggregate. - * - *

This is the value specified with {@code INITCOND} in the {@code CREATE AGGREGATE...} - * statement. It's passed to the initial invocation of the state function (if that function does - * not accept null arguments). - * - *

The actual type of the returned object depends on the aggregate's {@link #getStateType() - * state type} and on the {@link TypeCodec codec} used to {@link TypeCodec#parse(String) parse} - * the {@code INITCOND} literal. - * - *

If, for some reason, the {@code INITCOND} literal cannot be parsed, a warning will be logged - * and the returned object will be the original {@code INITCOND} literal in its textual, - * non-parsed form. - * - * @return the initial state, or {@code null} if there is none. - */ - public Object getInitCond() { - return initCond; - } - - /** - * Returns the return type of this aggregate. - * - *

This is the final type of the value computed by this aggregate; in other words, the return - * type of the final function if it is defined, or the state type otherwise. - * - * @return the return type. - */ - public DataType getReturnType() { - return returnType; - } - - /** - * Returns the state function of this aggregate. - * - *

This is the function specified with {@code SFUNC} in the {@code CREATE AGGREGATE...} - * statement. It aggregates the current state with each row to produce a new state. - * - * @return the metadata of the state function. - */ - public FunctionMetadata getStateFunc() { - return keyspace.functions.get(stateFuncFullName); - } - - /** - * Returns the state type of this aggregate. - * - *

This is the type specified with {@code STYPE} in the {@code CREATE AGGREGATE...} statement. - * It defines the type of the value that is accumulated as the aggregate iterates through the - * rows. - * - * @return the state type. - */ - public DataType getStateType() { - return stateType; - } - - @Override - public boolean equals(Object other) { - if (other == this) return true; - - if (other instanceof AggregateMetadata) { - AggregateMetadata that = (AggregateMetadata) other; - return this.keyspace.getName().equals(that.keyspace.getName()) - && this.argumentTypes.equals(that.argumentTypes) - && MoreObjects.equal(this.finalFuncFullName, that.finalFuncFullName) - && - // Note: this might be a problem if a custom codec has been registered for the initCond's - // type, with a target Java type that - // does not properly implement equals. We don't have any control over this, at worst this - // would lead to spurious change - // notifications. - MoreObjects.equal(this.initCond, that.initCond) - && this.returnType.equals(that.returnType) - && this.stateFuncFullName.equals(that.stateFuncFullName) - && this.stateType.equals(that.stateType); - } - return false; - } - - @Override - public int hashCode() { - return MoreObjects.hashCode( - this.keyspace.getName(), - this.argumentTypes, - this.finalFuncFullName, - this.initCond, - this.returnType, - this.stateFuncFullName, - this.stateType); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ArrayBackedResultSet.java b/driver-core/src/main/java/com/datastax/driver/core/ArrayBackedResultSet.java deleted file mode 100644 index 3337ec31d45..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ArrayBackedResultSet.java +++ /dev/null @@ -1,592 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.ConnectionException; -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import com.google.common.util.concurrent.Uninterruptibles; -import java.nio.ByteBuffer; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Deque; -import java.util.Iterator; -import java.util.List; -import java.util.Queue; -import java.util.UUID; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.LinkedBlockingDeque; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Default implementation of a result set, backed by an ArrayDeque of ArrayList. */ -abstract class ArrayBackedResultSet implements ResultSet { - - private static final Logger logger = LoggerFactory.getLogger(ResultSet.class); - - private static final Queue> EMPTY_QUEUE = new ArrayDeque>(0); - - protected volatile ColumnDefinitions metadata; - protected final Token.Factory tokenFactory; - private final boolean wasApplied; - - protected final ProtocolVersion protocolVersion; - protected final CodecRegistry codecRegistry; - - private ArrayBackedResultSet( - ColumnDefinitions metadata, - Token.Factory tokenFactory, - List firstRow, - ProtocolVersion protocolVersion, - CodecRegistry codecRegistry) { - this.metadata = metadata; - this.protocolVersion = protocolVersion; - this.codecRegistry = codecRegistry; - this.tokenFactory = tokenFactory; - this.wasApplied = checkWasApplied(firstRow, metadata, protocolVersion); - } - - static ArrayBackedResultSet fromMessage( - Responses.Result msg, - SessionManager session, - ProtocolVersion protocolVersion, - ExecutionInfo info, - Statement statement) { - - switch (msg.kind) { - case ROWS: - Responses.Result.Rows r = (Responses.Result.Rows) msg; - - Statement actualStatement = statement; - if (statement instanceof StatementWrapper) { - actualStatement = ((StatementWrapper) statement).getWrappedStatement(); - } - - ColumnDefinitions columnDefs = r.metadata.columns; - if (columnDefs == null) { - // If result set metadata is not present, it means the request had SKIP_METADATA set, the - // driver - // only ever does that for bound statements. - BoundStatement bs = (BoundStatement) actualStatement; - columnDefs = bs.preparedStatement().getPreparedId().resultSetMetadata.variables; - } else { - // Otherwise, always use the response's metadata. - // In addition, if a new id is present it means we're executing a bound statement with - // protocol v5, - // the schema changed server-side, and we need to update the prepared statement (see - // CASSANDRA-10786). - MD5Digest newMetadataId = r.metadata.metadataId; - assert !(actualStatement instanceof BoundStatement) - || ProtocolFeature.PREPARED_METADATA_CHANGES.isSupportedBy(protocolVersion) - || newMetadataId == null; - if (newMetadataId != null) { - BoundStatement bs = ((BoundStatement) actualStatement); - PreparedId preparedId = bs.preparedStatement().getPreparedId(); - preparedId.resultSetMetadata = - new PreparedId.PreparedMetadata(newMetadataId, columnDefs); - } - } - assert columnDefs != null; - - Token.Factory tokenFactory = - (session == null) ? null : session.getCluster().manager.metadata.tokenFactory(); - - info = - update( - info, - r, - session, - r.metadata.pagingState, - protocolVersion, - columnDefs.codecRegistry, - statement); - - // info can be null only for internal calls, but we don't page those. We assert - // this explicitly because MultiPage implementation doesn't support info == null. - assert r.metadata.pagingState == null || info != null; - - return r.metadata.pagingState == null - ? new SinglePage( - columnDefs, tokenFactory, protocolVersion, columnDefs.codecRegistry, r.data, info) - : new MultiPage( - columnDefs, - tokenFactory, - protocolVersion, - columnDefs.codecRegistry, - r.data, - info, - r.metadata.pagingState, - session); - - case VOID: - case SET_KEYSPACE: - case SCHEMA_CHANGE: - info = update(info, msg, session, null, protocolVersion, null, statement); - return empty(info); - case PREPARED: - throw new RuntimeException("Prepared statement received when a ResultSet was expected"); - default: - logger.error("Received unknown result type '{}'; returning empty result set", msg.kind); - info = update(info, msg, session, null, protocolVersion, null, statement); - return empty(info); - } - } - - private static ExecutionInfo update( - ExecutionInfo info, - Responses.Result msg, - SessionManager session, - ByteBuffer pagingState, - ProtocolVersion protocolVersion, - CodecRegistry codecRegistry, - Statement statement) { - if (info == null) return null; - - UUID tracingId = msg.getTracingId(); - QueryTrace trace = (tracingId == null) ? null : new QueryTrace(tracingId, session); - - return info.with(trace, msg.warnings, pagingState, statement, protocolVersion, codecRegistry); - } - - private static ArrayBackedResultSet empty(ExecutionInfo info) { - // We could pass the protocol version but we know we won't need it so passing a bogus value - // (null) - return new SinglePage(ColumnDefinitions.EMPTY, null, null, null, EMPTY_QUEUE, info); - } - - @Override - public ColumnDefinitions getColumnDefinitions() { - return metadata; - } - - @Override - public List all() { - if (isExhausted()) return Collections.emptyList(); - - // We may have more than 'getAvailableWithoutFetching' results but we won't have less, and - // at least in the single page case this will be exactly the size we want so ... - List result = new ArrayList(getAvailableWithoutFetching()); - for (Row row : this) result.add(row); - return result; - } - - @Override - public Iterator iterator() { - return new Iterator() { - - @Override - public boolean hasNext() { - return !isExhausted(); - } - - @Override - public Row next() { - return ArrayBackedResultSet.this.one(); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - - @Override - public boolean wasApplied() { - return wasApplied; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("ResultSet[ exhausted: ").append(isExhausted()); - sb.append(", ").append(metadata).append(']'); - return sb.toString(); - } - - private static class SinglePage extends ArrayBackedResultSet { - - private final Queue> rows; - private final ExecutionInfo info; - - private SinglePage( - ColumnDefinitions metadata, - Token.Factory tokenFactory, - ProtocolVersion protocolVersion, - CodecRegistry codecRegistry, - Queue> rows, - ExecutionInfo info) { - super(metadata, tokenFactory, rows.peek(), protocolVersion, codecRegistry); - this.info = info; - this.rows = rows; - } - - @Override - public boolean isExhausted() { - return rows.isEmpty(); - } - - @Override - public Row one() { - return ArrayBackedRow.fromData(metadata, tokenFactory, protocolVersion, rows.poll()); - } - - @Override - public int getAvailableWithoutFetching() { - return rows.size(); - } - - @Override - public boolean isFullyFetched() { - return true; - } - - @Override - public ListenableFuture fetchMoreResults() { - return Futures.immediateFuture(this); - } - - @Override - public ExecutionInfo getExecutionInfo() { - return info; - } - - @Override - public List getAllExecutionInfo() { - return Collections.singletonList(info); - } - } - - private static class MultiPage extends ArrayBackedResultSet { - - private Queue> currentPage; - private final Queue nextPages = new ConcurrentLinkedQueue(); - - private final Deque infos = new LinkedBlockingDeque(); - - /* - * The fetching state of this result set. The fetchState will always be in one of - * the 3 following state: - * 1) fetchState is null or reference a null: fetching is done, there - * is nothing more to fetch and no query in progress. - * 2) fetchState.get().nextStart is not null: there is more pages to fetch. In - * that case, inProgress is *guaranteed* to be null. - * 3) fetchState.get().inProgress is not null: a page is being fetched. - * In that case, nextStart is *guaranteed* to be null. - * - * Also note that while ResultSet doesn't pretend to be thread-safe, the actual - * fetch is done asynchronously and so we do need to be volatile below. - */ - private volatile FetchingState fetchState; - - private final SessionManager session; - - private MultiPage( - ColumnDefinitions metadata, - Token.Factory tokenFactory, - ProtocolVersion protocolVersion, - CodecRegistry codecRegistry, - Queue> rows, - ExecutionInfo info, - ByteBuffer pagingState, - SessionManager session) { - - // Note: as of Cassandra 2.1.0, it turns out that the result of a CAS update is never paged, - // so - // we could hard-code the result of wasApplied in this class to "true". However, we can not be - // sure - // that this will never change, so apply the generic check by peeking at the first row. - super(metadata, tokenFactory, rows.peek(), protocolVersion, codecRegistry); - this.currentPage = rows; - this.infos.offer(info); - - this.fetchState = new FetchingState(pagingState, null); - this.session = session; - } - - @Override - public boolean isExhausted() { - prepareNextRow(); - return currentPage.isEmpty(); - } - - @Override - public Row one() { - prepareNextRow(); - return ArrayBackedRow.fromData(metadata, tokenFactory, protocolVersion, currentPage.poll()); - } - - @Override - public int getAvailableWithoutFetching() { - int available = currentPage.size(); - for (NextPage page : nextPages) available += page.data.size(); - return available; - } - - @Override - public boolean isFullyFetched() { - return fetchState == null; - } - - // Ensure that after the call the next row to consume is in 'currentPage', i.e. that - // 'currentPage' is empty IFF the ResultSet if fully exhausted. - private void prepareNextRow() { - while (currentPage.isEmpty()) { - // Grab the current state now to get a consistent view in this iteration. - FetchingState fetchingState = this.fetchState; - - NextPage nextPage = nextPages.poll(); - if (nextPage != null) { - if (nextPage.metadata != null) { - this.metadata = nextPage.metadata; - } - currentPage = nextPage.data; - continue; - } - if (fetchingState == null) return; - - // We need to know if there is more result, so fetch the next page and - // wait on it. - try { - session.checkNotInEventLoop(); - Uninterruptibles.getUninterruptibly(fetchMoreResults()); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } - } - - @Override - public ListenableFuture fetchMoreResults() { - return fetchMoreResults(this.fetchState); - } - - private ListenableFuture fetchMoreResults(FetchingState fetchState) { - if (fetchState == null) return Futures.immediateFuture(this); - - if (fetchState.inProgress != null) return fetchState.inProgress; - - assert fetchState.nextStart != null; - ByteBuffer state = fetchState.nextStart; - SettableFuture future = SettableFuture.create(); - this.fetchState = new FetchingState(null, future); - return queryNextPage(state, future); - } - - private ListenableFuture queryNextPage( - ByteBuffer nextStart, final SettableFuture future) { - - Statement statement = this.infos.peek().getStatement(); - - assert !(statement instanceof BatchStatement); - - final Message.Request request = session.makeRequestMessage(statement, nextStart); - session.execute( - new RequestHandler.Callback() { - - @Override - public Message.Request request() { - return request; - } - - @Override - public void register(RequestHandler handler) {} - - @Override - public void onSet( - Connection connection, - Message.Response response, - ExecutionInfo info, - Statement statement, - long latency) { - try { - switch (response.type) { - case RESULT: - Responses.Result rm = (Responses.Result) response; - if (rm.kind == Responses.Result.Kind.ROWS) { - Responses.Result.Rows rows = (Responses.Result.Rows) rm; - info = - update( - info, - rm, - MultiPage.this.session, - rows.metadata.pagingState, - protocolVersion, - codecRegistry, - statement); - // If the query is a prepared 'SELECT *', the metadata can change between - // pages - ColumnDefinitions newMetadata = null; - if (rows.metadata.metadataId != null) { - newMetadata = rows.metadata.columns; - assert statement instanceof BoundStatement; - BoundStatement bs = (BoundStatement) statement; - bs.preparedStatement().getPreparedId().resultSetMetadata = - new PreparedId.PreparedMetadata( - rows.metadata.metadataId, rows.metadata.columns); - } - MultiPage.this.nextPages.offer(new NextPage(newMetadata, rows.data)); - MultiPage.this.fetchState = - rows.metadata.pagingState == null - ? null - : new FetchingState(rows.metadata.pagingState, null); - } else if (rm.kind == Responses.Result.Kind.VOID) { - // We shouldn't really get a VOID message here but well, no harm in handling - // it I suppose - info = - update( - info, - rm, - MultiPage.this.session, - null, - protocolVersion, - codecRegistry, - statement); - MultiPage.this.fetchState = null; - } else { - logger.error( - "Received unknown result type '{}' during paging: ignoring message", - rm.kind); - // This mean we have probably have a bad node, so defunct the connection - connection.defunct( - new ConnectionException( - connection.endPoint, - String.format("Got unexpected %s result response", rm.kind))); - future.setException( - new DriverInternalError( - String.format( - "Got unexpected %s result response from %s", - rm.kind, connection.endPoint))); - return; - } - - MultiPage.this.infos.offer(info); - future.set(MultiPage.this); - break; - case ERROR: - future.setException( - ((Responses.Error) response).asException(connection.endPoint)); - break; - default: - // This mean we have probably have a bad node, so defunct the connection - connection.defunct( - new ConnectionException( - connection.endPoint, - String.format("Got unexpected %s response", response.type))); - future.setException( - new DriverInternalError( - String.format( - "Got unexpected %s response from %s", - response.type, connection.endPoint))); - break; - } - } catch (RuntimeException e) { - // If we get a bug here, the client will not get it, so better forwarding the error - future.setException( - new DriverInternalError( - "Unexpected error while processing response from " + connection.endPoint, - e)); - } - } - - // This is only called for internal calls, so don't bother with ExecutionInfo - @Override - public void onSet( - Connection connection, Message.Response response, long latency, int retryCount) { - onSet(connection, response, null, null, latency); - } - - @Override - public void onException( - Connection connection, Exception exception, long latency, int retryCount) { - future.setException(exception); - } - - @Override - public boolean onTimeout(Connection connection, long latency, int retryCount) { - // This won't be called directly since this will be wrapped by RequestHandler. - throw new UnsupportedOperationException(); - } - - @Override - public int retryCount() { - // This is only called for internal calls (i.e, when the callback is not wrapped in - // RequestHandler). - // There is no retry logic in that case, so the value does not really matter. - return 0; - } - }, - statement); - - return future; - } - - @Override - public ExecutionInfo getExecutionInfo() { - return infos.getLast(); - } - - @Override - public List getAllExecutionInfo() { - return new ArrayList(infos); - } - - private static class FetchingState { - public final ByteBuffer nextStart; - public final ListenableFuture inProgress; - - FetchingState(ByteBuffer nextStart, ListenableFuture inProgress) { - assert (nextStart == null) != (inProgress == null); - this.nextStart = nextStart; - this.inProgress = inProgress; - } - } - - private static class NextPage { - final ColumnDefinitions metadata; - final Queue> data; - - NextPage(ColumnDefinitions metadata, Queue> data) { - this.metadata = metadata; - this.data = data; - } - } - } - - // This method checks the value of the "[applied]" column manually, to avoid instantiating an - // ArrayBackedRow - // object that we would throw away immediately. - private static boolean checkWasApplied( - List firstRow, ColumnDefinitions metadata, ProtocolVersion protocolVersion) { - // If the column is not present or not a boolean, we assume the query - // was not a conditional statement, and therefore return true. - if (firstRow == null) return true; - int[] is = metadata.findAllIdx("[applied]"); - if (is == null) return true; - int i = is[0]; - if (!DataType.cboolean().equals(metadata.getType(i))) return true; - - // Otherwise return the value of the column - ByteBuffer value = firstRow.get(i); - if (value == null || value.remaining() == 0) return false; - - return TypeCodec.cboolean().deserializeNoBoxing(value, protocolVersion); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ArrayBackedRow.java b/driver-core/src/main/java/com/datastax/driver/core/ArrayBackedRow.java deleted file mode 100644 index 8e7028035ed..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ArrayBackedRow.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.regex.Pattern; - -/** Implementation of a Row backed by an ArrayList. */ -class ArrayBackedRow extends AbstractGettableData implements Row { - - /** - * A pattern to parse (non-aliased) token column names of the form token(x). Note that starting - * from Cassandra 2.2 built-in functions are declared in the system keyspace, so the function name - * is prefixed with "system.". - */ - private static final Pattern TOKEN_COLUMN_NAME = Pattern.compile("(system\\.)?token(.*)"); - - private final ColumnDefinitions metadata; - private final Token.Factory tokenFactory; - private final List data; - - private ArrayBackedRow( - ColumnDefinitions metadata, - Token.Factory tokenFactory, - ProtocolVersion protocolVersion, - List data) { - super(protocolVersion); - this.metadata = metadata; - this.tokenFactory = tokenFactory; - this.data = data; - } - - static Row fromData( - ColumnDefinitions metadata, - Token.Factory tokenFactory, - ProtocolVersion protocolVersion, - List data) { - if (data == null) return null; - - return new ArrayBackedRow(metadata, tokenFactory, protocolVersion, data); - } - - @Override - public ColumnDefinitions getColumnDefinitions() { - return metadata; - } - - @Override - protected DataType getType(int i) { - return metadata.getType(i); - } - - @Override - protected String getName(int i) { - return metadata.getName(i); - } - - @Override - protected ByteBuffer getValue(int i) { - return data.get(i); - } - - @Override - protected CodecRegistry getCodecRegistry() { - return metadata.codecRegistry; - } - - @Override - protected int getIndexOf(String name) { - return metadata.getFirstIdx(name); - } - - @Override - public Token getToken(int i) { - if (tokenFactory == null) - throw new DriverInternalError( - "Token factory not set. This should only happen at initialization time"); - - checkType(i, tokenFactory.getTokenType().getName()); - - ByteBuffer value = data.get(i); - if (value == null || value.remaining() == 0) return null; - - return tokenFactory.deserialize(value, protocolVersion); - } - - @Override - public Token getToken(String name) { - return getToken(metadata.getFirstIdx(name)); - } - - @Override - public Token getPartitionKeyToken() { - int i = 0; - for (ColumnDefinitions.Definition column : metadata) { - if (TOKEN_COLUMN_NAME.matcher(column.getName()).matches()) return getToken(i); - i++; - } - throw new IllegalStateException( - "Found no column named 'token(...)'. If the column is aliased, use getToken(String)."); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("Row["); - for (int i = 0; i < metadata.size(); i++) { - if (i != 0) sb.append(", "); - ByteBuffer bb = data.get(i); - if (bb == null) sb.append("NULL"); - else { - Object o = - getCodecRegistry().codecFor(metadata.getType(i)).deserialize(bb, protocolVersion); - if (o == null) { - sb.append("NULL"); - } else { - sb.append(o.toString()); - } - } - } - sb.append(']'); - return sb.toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/AtomicMonotonicTimestampGenerator.java b/driver-core/src/main/java/com/datastax/driver/core/AtomicMonotonicTimestampGenerator.java deleted file mode 100644 index f20b34e5451..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AtomicMonotonicTimestampGenerator.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -/** - * A timestamp generator that guarantees monotonically increasing timestamps among all client - * threads, and logs warnings when timestamps drift in the future. - * - * @see AbstractMonotonicTimestampGenerator - */ -public class AtomicMonotonicTimestampGenerator extends LoggingMonotonicTimestampGenerator { - - private AtomicLong lastRef = new AtomicLong(0); - - /** - * Creates a new instance with a warning threshold and warning interval of one second. - * - * @see #AtomicMonotonicTimestampGenerator(long, TimeUnit, long, TimeUnit) - */ - public AtomicMonotonicTimestampGenerator() { - this(1, TimeUnit.SECONDS, 1, TimeUnit.SECONDS); - } - - /** - * Creates a new instance. - * - * @param warningThreshold how far in the future timestamps are allowed to drift before a warning - * is logged. - * @param warningThresholdUnit the unit for {@code warningThreshold}. - * @param warningInterval how often the warning will be logged if timestamps keep drifting above - * the threshold. - * @param warningIntervalUnit the unit for {@code warningIntervalUnit}. - */ - public AtomicMonotonicTimestampGenerator( - long warningThreshold, - TimeUnit warningThresholdUnit, - long warningInterval, - TimeUnit warningIntervalUnit) { - super(warningThreshold, warningThresholdUnit, warningInterval, warningIntervalUnit); - } - - @Override - public long next() { - while (true) { - long last = lastRef.get(); - long next = computeNext(last); - if (lastRef.compareAndSet(last, next)) return next; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/AuthProvider.java b/driver-core/src/main/java/com/datastax/driver/core/AuthProvider.java deleted file mode 100644 index b6ace5df32f..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/AuthProvider.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.AuthenticationException; -import java.net.InetSocketAddress; - -/** - * Provides {@link Authenticator} instances for use when connecting to Cassandra nodes. - * - *

See {@link PlainTextAuthProvider} for an implementation which uses SASL PLAIN mechanism to - * authenticate using username/password strings - */ -public interface AuthProvider { - - /** - * A provider that provides no authentication capability. - * - *

This is only useful as a placeholder when no authentication is to be used. - */ - AuthProvider NONE = new ExtendedAuthProvider.NoAuthProvider(); - - /** - * The {@code Authenticator} to use when connecting to {@code host} - * - * @param host the Cassandra host to connect to. - * @param authenticator the configured authenticator on the host. - * @return The authentication implementation to use. - */ - public Authenticator newAuthenticator(InetSocketAddress host, String authenticator) - throws AuthenticationException; - - /** - * Dummy Authenticator that accounts for DSE authentication configured with transitional mode. - * - *

In this situation, the client is allowed to connect without authentication, but DSE would - * still send an AUTHENTICATE response. This Authenticator handles this situation by sending back - * a dummy credential. - */ - class TransitionalModePlainTextAuthenticator - extends PlainTextAuthProvider.PlainTextAuthenticator { - - public TransitionalModePlainTextAuthenticator() { - super("", ""); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Authenticator.java b/driver-core/src/main/java/com/datastax/driver/core/Authenticator.java deleted file mode 100644 index 00e2a48279a..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Authenticator.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Handles SASL authentication with Cassandra servers. - * - *

Each time a new connection is created and the server requires authentication, a new instance - * of this class will be created by the corresponding {@link AuthProvider} to handle that - * authentication. The lifecycle of that new {@code Authenticator} will be: - * - *

    - *
  1. The {@code initialResponse} method will be called. The initial return value will be sent to - * the server to initiate the handshake. - *
  2. The server will respond to each client response by either issuing a challenge or indicating - * that the authentication is complete (successfully or not). If a new challenge is issued, - * the authenticator {@code evaluateChallenge} method will be called to produce a response - * that will be sent to the server. This challenge/response negotiation will continue until - * the server responds that authentication is successful (or an {@code - * AuthenticationException} is raised). - *
  3. When the server indicates that authentication is successful, the {@code - * onAuthenticationSuccess} method will be called with the last information that the server - * may optionally have sent. - *
- * - * The exact nature of the negotiation between client and server is specific to the authentication - * mechanism configured server side. - */ -public interface Authenticator { - - /** - * Obtain an initial response token for initializing the SASL handshake - * - * @return the initial response to send to the server, may be null - */ - public byte[] initialResponse(); - - /** - * Evaluate a challenge received from the Server. Generally, this method should return null when - * authentication is complete from the client perspective - * - * @param challenge the server's SASL challenge - * @return updated SASL token, may be null to indicate the client requires no further action - */ - public byte[] evaluateChallenge(byte[] challenge); - - /** - * Called when authentication is successful with the last information optionally sent by the - * server. - * - * @param token the information sent by the server with the authentication successful message. - * This will be {@code null} if the server sends no particular information on authentication - * success. - */ - public void onAuthenticationSuccess(byte[] token); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/BatchStatement.java b/driver-core/src/main/java/com/datastax/driver/core/BatchStatement.java deleted file mode 100644 index e2b7a805483..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/BatchStatement.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.Frame.Header; -import com.datastax.driver.core.Requests.QueryFlag; -import com.datastax.driver.core.exceptions.UnsupportedFeatureException; -import com.google.common.collect.ImmutableList; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * A statement that groups a number of {@link Statement} so they get executed as a batch. - * - *

Note: BatchStatement is not supported with the native protocol version 1: you will get an - * {@link UnsupportedFeatureException} when submitting one if version 1 of the protocol is in use - * (i.e. if you've force version 1 through {@link Cluster.Builder#withProtocolVersion} or you use - * Cassandra 1.2). Note however that you can still use CQL Batch statements even with - * the protocol version 1. - * - *

Setting a BatchStatement's serial consistency level is only supported with the native protocol - * version 3 or higher (see {@link #setSerialConsistencyLevel(ConsistencyLevel)}). - */ -public class BatchStatement extends Statement { - - /** The type of batch to use. */ - public enum Type { - /** - * A logged batch: Cassandra will first write the batch to its distributed batch log to ensure - * the atomicity of the batch (atomicity meaning that if any statement in the batch succeeds, - * all will eventually succeed). - */ - LOGGED, - - /** - * A batch that doesn't use Cassandra's distributed batch log. Such batch are not guaranteed to - * be atomic. - */ - UNLOGGED, - - /** - * A counter batch. Note that such batch is the only type that can contain counter operations - * and it can only contain these. - */ - COUNTER - } - - final Type batchType; - private final List statements = new ArrayList(); - - /** Creates a new {@code LOGGED} batch statement. */ - public BatchStatement() { - this(Type.LOGGED); - } - - /** - * Creates a new batch statement of the provided type. - * - * @param batchType the type of batch. - */ - public BatchStatement(Type batchType) { - this.batchType = batchType; - } - - IdAndValues getIdAndValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - IdAndValues idAndVals = new IdAndValues(statements.size()); - for (int i = 0; i < statements.size(); i++) { - Statement statement = statements.get(i); - if (statement instanceof StatementWrapper) { - statement = ((StatementWrapper) statement).getWrappedStatement(); - } - if (statement instanceof RegularStatement) { - RegularStatement st = (RegularStatement) statement; - ByteBuffer[] vals = st.getValues(protocolVersion, codecRegistry); - String query = st.getQueryString(codecRegistry); - idAndVals.ids.add(query); - idAndVals.values[i] = vals == null ? Requests.EMPTY_BB_ARRAY : vals; - } else { - // We handle BatchStatement in add() so ... - assert statement instanceof BoundStatement; - BoundStatement st = (BoundStatement) statement; - idAndVals.ids.add(st.statement.getPreparedId().boundValuesMetadata.id); - idAndVals.values[i] = st.wrapper.values; - } - } - return idAndVals; - } - - /** - * Adds a new statement to this batch. - * - *

Note that {@code statement} can be any {@code Statement}. It is allowed to mix {@code - * RegularStatement} and {@code BoundStatement} in the same {@code BatchStatement} in particular. - * Adding another {@code BatchStatement} is also allowed for convenience and is equivalent to - * adding all the {@code Statement} contained in that other {@code BatchStatement}. - * - *

Due to a protocol-level limitation, adding a {@code RegularStatement} with named values is - * currently not supported; an {@code IllegalArgument} will be thrown. - * - *

When adding a {@code BoundStatement}, all of its values must be set, otherwise an {@code - * IllegalStateException} will be thrown when submitting the batch statement. See {@link - * BoundStatement} for more details, in particular how to handle {@code null} values. - * - *

Please note that the options of the added Statement (all those defined directly by the - * {@link Statement} class: consistency level, fetch size, tracing, ...) will be ignored for the - * purpose of the execution of the Batch. Instead, the options used are the one of this {@code - * BatchStatement} object. - * - * @param statement the new statement to add. - * @return this batch statement. - * @throws IllegalStateException if adding the new statement means that this {@code - * BatchStatement} has more than 65536 statements (since this is the maximum number of - * statements for a BatchStatement allowed by the underlying protocol). - * @throws IllegalArgumentException if adding a regular statement that uses named values. - */ - public BatchStatement add(Statement statement) { - if (statement instanceof StatementWrapper) { - statement = ((StatementWrapper) statement).getWrappedStatement(); - } - if ((statement instanceof RegularStatement) - && ((RegularStatement) statement).usesNamedValues()) { - throw new IllegalArgumentException( - "Batch statement cannot contain regular statements with named values (" - + ((RegularStatement) statement).getQueryString() - + ")"); - } - - // We handle BatchStatement here (rather than in getIdAndValues) as it make it slightly - // easier to avoid endless loops if the user mistakenly passes a batch that depends on this - // object (or this directly). - if (statement instanceof BatchStatement) { - for (Statement subStatements : ((BatchStatement) statement).statements) { - add(subStatements); - } - } else { - if (statements.size() >= 0xFFFF) - throw new IllegalStateException( - "Batch statement cannot contain more than " + 0xFFFF + " statements."); - statements.add(statement); - } - return this; - } - - /** - * Adds multiple statements to this batch. - * - *

This is a shortcut method that calls {@link #add} on all the statements from {@code - * statements}. - * - * @param statements the statements to add. - * @return this batch statement. - */ - public BatchStatement addAll(Iterable statements) { - for (Statement statement : statements) add(statement); - return this; - } - - /** - * The statements that have been added to this batch so far. - * - * @return an (immutable) collection of the statements that have been added to this batch so far. - */ - public Collection getStatements() { - return ImmutableList.copyOf(statements); - } - - /** - * Clears this batch, removing all statements added so far. - * - * @return this (now empty) {@code BatchStatement}. - */ - public BatchStatement clear() { - statements.clear(); - return this; - } - - /** - * Returns the number of elements in this batch. - * - * @return the number of elements in this batch. - */ - public int size() { - return statements.size(); - } - - @Override - public int requestSizeInBytes(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - int size = Header.lengthFor(protocolVersion) + 3; // type + nb queries - try { - BatchStatement.IdAndValues idAndVals = getIdAndValues(protocolVersion, codecRegistry); - for (int i = 0; i < idAndVals.ids.size(); i++) { - Object q = idAndVals.ids.get(i); - size += - 1 - + (q instanceof String - ? CBUtil.sizeOfLongString((String) q) - : CBUtil.sizeOfShortBytes(((MD5Digest) q).bytes)); - size += CBUtil.sizeOfValueList(idAndVals.values[i]); - } - switch (protocolVersion) { - case V2: - size += CBUtil.sizeOfConsistencyLevel(getConsistencyLevel()); - break; - case V3: - case V4: - case V5: - case V6: - size += CBUtil.sizeOfConsistencyLevel(getConsistencyLevel()); - size += QueryFlag.serializedSize(protocolVersion); - // Serial CL and default timestamp also depend on session-level defaults (QueryOptions). - // We always count them to avoid having to inject QueryOptions here, at worst we - // overestimate by a - // few bytes. - size += CBUtil.sizeOfConsistencyLevel(getSerialConsistencyLevel()); - if (ProtocolFeature.CLIENT_TIMESTAMPS.isSupportedBy(protocolVersion)) { - size += 8; // timestamp - } - if (ProtocolFeature.CUSTOM_PAYLOADS.isSupportedBy(protocolVersion) - && getOutgoingPayload() != null) { - size += CBUtil.sizeOfBytesMap(getOutgoingPayload()); - } - break; - default: - throw protocolVersion.unsupported(); - } - } catch (Exception e) { - size = -1; - } - return size; - } - - /** - * Sets the serial consistency level for the query. - * - *

This is only supported with version 3 or higher of the native protocol. If you call this - * method when version 2 is in use, you will get an {@link UnsupportedFeatureException} when - * submitting the statement. With version 2, protocol batches with conditions have their serial - * consistency level hardcoded to SERIAL; if you need to execute a batch with LOCAL_SERIAL, you - * will have to use a CQL batch. - * - * @param serialConsistency the serial consistency level to set. - * @return this {@code Statement} object. - * @throws IllegalArgumentException if {@code serialConsistency} is not one of {@code - * ConsistencyLevel.SERIAL} or {@code ConsistencyLevel.LOCAL_SERIAL}. - * @see Statement#setSerialConsistencyLevel(ConsistencyLevel) - */ - @Override - public BatchStatement setSerialConsistencyLevel(ConsistencyLevel serialConsistency) { - return (BatchStatement) super.setSerialConsistencyLevel(serialConsistency); - } - - @Override - public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - for (Statement statement : statements) { - if (statement instanceof StatementWrapper) - statement = ((StatementWrapper) statement).getWrappedStatement(); - ByteBuffer rk = statement.getRoutingKey(protocolVersion, codecRegistry); - if (rk != null) return rk; - } - return null; - } - - @Override - public String getKeyspace() { - for (Statement statement : statements) { - String keyspace = statement.getKeyspace(); - if (keyspace != null) return keyspace; - } - return null; - } - - @Override - public Boolean isIdempotent() { - if (idempotent != null) { - return idempotent; - } - return isBatchIdempotent(statements); - } - - void ensureAllSet() { - for (Statement statement : statements) - if (statement instanceof BoundStatement) ((BoundStatement) statement).ensureAllSet(); - } - - static class IdAndValues { - - public final List ids; - public final ByteBuffer[][] values; - - IdAndValues(int nbstatements) { - ids = new ArrayList(nbstatements); - values = new ByteBuffer[nbstatements][]; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/BoundStatement.java b/driver-core/src/main/java/com/datastax/driver/core/BoundStatement.java deleted file mode 100644 index 01c39dfc232..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/BoundStatement.java +++ /dev/null @@ -1,1217 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.Frame.Header; -import com.datastax.driver.core.Requests.QueryFlag; -import com.datastax.driver.core.exceptions.InvalidTypeException; -import com.google.common.reflect.TypeToken; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -/** - * A prepared statement with values bound to the bind variables. - * - *

Once values has been provided for the variables of the {@link PreparedStatement} it has been - * created from, such BoundStatement can be executed (through {@link Session#execute(Statement)}). - * - *

The values of a BoundStatement can be set by either index or name. When setting them by name, - * names follow the case insensitivity rules explained in {@link ColumnDefinitions} but with the - * difference that if multiple bind variables have the same name, setting that name will set - * all the variables for that name. - * - *

With native protocol V3 or below, all variables of the statement must be bound. If you don't - * explicitly set a value for a variable, an {@code IllegalStateException} will be thrown when - * submitting the statement. If you want to set a variable to {@code null}, use {@link - * #setToNull(int) setToNull}. - * - *

With native protocol V4 or above, variables can be left unset, in which case they will be - * ignored server side (no tombstones will be generated). If you're reusing a bound statement, you - * can {@link #unset(int) unset} variables that were previously set. - * - *

This class is not thread-safe. Do not share instances among requests that will execute - * concurrently (e.g. requests run from separate application threads, but also separate {@link - * Session#executeAsync(Statement) executeAsync} calls, even if they're triggered from the same - * thread). - */ -public class BoundStatement extends Statement - implements SettableData, GettableData { - static final ByteBuffer UNSET = ByteBuffer.allocate(0); - - final PreparedStatement statement; - - // Statement is already an abstract class, so we can't make it extend AbstractData directly. But - // we still want to avoid duplicating too much code so we wrap. - final DataWrapper wrapper; - - private final CodecRegistry codecRegistry; - - private ByteBuffer routingKey; - - /** - * Creates a new {@code BoundStatement} from the provided prepared statement. - * - * @param statement the prepared statement from which to create a {@code BoundStatement}. - */ - public BoundStatement(PreparedStatement statement) { - this.statement = statement; - this.wrapper = new DataWrapper(this, statement.getVariables().size()); - for (int i = 0; i < wrapper.values.length; i++) { - wrapper.values[i] = UNSET; - } - - if (statement.getConsistencyLevel() != null) - this.setConsistencyLevel(statement.getConsistencyLevel()); - if (statement.getSerialConsistencyLevel() != null) - this.setSerialConsistencyLevel(statement.getSerialConsistencyLevel()); - if (statement.isTracing()) this.enableTracing(); - if (statement.getRetryPolicy() != null) this.setRetryPolicy(statement.getRetryPolicy()); - if (statement.getOutgoingPayload() != null) - this.setOutgoingPayload(statement.getOutgoingPayload()); - else - // propagate incoming payload as outgoing payload, if no outgoing payload has been explicitly - // set - this.setOutgoingPayload(statement.getIncomingPayload()); - this.codecRegistry = statement.getCodecRegistry(); - if (statement.isIdempotent() != null) { - this.setIdempotent(statement.isIdempotent()); - } - } - - /** - * Returns the prepared statement on which this BoundStatement is based. - * - * @return the prepared statement on which this BoundStatement is based. - */ - public PreparedStatement preparedStatement() { - return statement; - } - - /** - * Returns whether the {@code i}th variable has been bound. - * - * @param i the index of the variable to check. - * @return whether the {@code i}th variable has been bound. - * @throws IndexOutOfBoundsException if {@code i < 0 || i >= - * this.preparedStatement().variables().size()}. - */ - public boolean isSet(int i) { - return wrapper.getValue(i) != UNSET; - } - - /** - * Returns whether the first occurrence of variable {@code name} has been bound. - * - * @param name the name of the variable to check. - * @return whether the first occurrence of variable {@code name} has been bound to a non-null - * value. - * @throws IllegalArgumentException if {@code name} is not a prepared variable, that is if {@code - * !this.preparedStatement().variables().names().contains(name)}. - */ - public boolean isSet(String name) { - return wrapper.getValue(wrapper.getIndexOf(name)) != UNSET; - } - - /** - * Unsets the {@code i}th variable. This will leave the statement in the same state as if no - * setter was ever called for this variable. - * - *

The treatment of unset variables depends on the native protocol version, see {@link - * BoundStatement} for explanations. - * - * @param i the index of the variable. - * @throws IndexOutOfBoundsException if {@code i < 0 || i >= - * this.preparedStatement().variables().size()}. - */ - public void unset(int i) { - wrapper.setValue(i, UNSET); - } - - /** - * Unsets all occurrences of variable {@code name}. This will leave the statement in the same - * state as if no setter was ever called for this variable. - * - *

The treatment of unset variables depends on the native protocol version, see {@link - * BoundStatement} for explanations. - * - * @param name the name of the variable. - * @throws IllegalArgumentException if {@code name} is not a prepared variable, that is if {@code - * !this.preparedStatement().variables().names().contains(name)}. - */ - public void unset(String name) { - for (int i : wrapper.getAllIndexesOf(name)) { - wrapper.setValue(i, UNSET); - } - } - - /** - * Bound values to the variables of this statement. - * - *

This is a convenience method to bind all the variables of the {@code BoundStatement} in one - * call. - * - * @param values the values to bind to the variables of the newly created BoundStatement. The - * first element of {@code values} will be bound to the first bind variable, etc. It is legal - * to provide fewer values than the statement has bound variables. In that case, the remaining - * variable need to be bound before execution. If more values than variables are provided - * however, an IllegalArgumentException wil be raised. - * @return this bound statement. - * @throws IllegalArgumentException if more {@code values} are provided than there is of bound - * variables in this statement. - * @throws InvalidTypeException if any of the provided value is not of correct type to be bound to - * the corresponding bind variable. - * @throws NullPointerException if one of {@code values} is a collection (List, Set or Map) - * containing a null value. Nulls are not supported in collections by CQL. - */ - public BoundStatement bind(Object... values) { - - if (values.length > statement.getVariables().size()) - throw new IllegalArgumentException( - String.format( - "Prepared statement has only %d variables, %d values provided", - statement.getVariables().size(), values.length)); - - for (int i = 0; i < values.length; i++) { - Object value = values[i]; - if (value == null) { - wrapper.values[i] = null; - } else { - ProtocolVersion protocolVersion = statement.getPreparedId().protocolVersion; - if (value instanceof Token) - // bypass CodecRegistry for token values - wrapper.values[i] = ((Token) value).serialize(protocolVersion); - else wrapper.values[i] = wrapper.codecFor(i, value).serialize(value, protocolVersion); - } - } - return this; - } - - /** - * The routing key for this bound query. - * - *

This method will return a non-{@code null} value if either of the following occur: - * - *

    - *
  • The routing key has been set directly through {@link BoundStatement#setRoutingKey}. - *
  • The routing key has been set through {@link PreparedStatement#setRoutingKey} for the - * {@code PreparedStatement} this statement has been built from. - *
  • All the columns composing the partition key are bound variables of this {@code - * BoundStatement}. The routing key will then be built using the values provided for these - * partition key columns. - *
- * - * Otherwise, {@code null} is returned. - * - *

- * - *

Note that if the routing key has been set through {@link BoundStatement#setRoutingKey}, then - * that takes precedence. If the routing key has been set through {@link - * PreparedStatement#setRoutingKey} then that is used next. If neither of those are set then it is - * computed. - * - * @param protocolVersion unused by this implementation (no internal serialization is required to - * compute the key). - * @param codecRegistry unused by this implementation (no internal serialization is required to - * compute the key). - * @return the routing key for this statement or {@code null}. - */ - @Override - public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - if (this.routingKey != null) { - return this.routingKey; - } - - if (statement.getRoutingKey() != null) { - return statement.getRoutingKey(); - } - - int[] rkIndexes = statement.getPreparedId().routingKeyIndexes; - if (rkIndexes != null) { - if (rkIndexes.length == 1) { - return wrapper.values[rkIndexes[0]]; - } else { - ByteBuffer[] components = new ByteBuffer[rkIndexes.length]; - for (int i = 0; i < components.length; ++i) { - ByteBuffer value = wrapper.values[rkIndexes[i]]; - if (value == null) return null; - components[i] = value; - } - return SimpleStatement.compose(components); - } - } - return null; - } - - /** - * Sets the routing key for this bound statement. - * - *

This is useful when the routing key can neither be set on the {@code PreparedStatement} this - * bound statement was built from, nor automatically computed from bound variables. In particular, - * this is the case if the partition key is composite and only some of its components are bound. - * - * @param routingKey the raw (binary) value to use as routing key. - * @return this {@code BoundStatement} object. - * @see BoundStatement#getRoutingKey - */ - public BoundStatement setRoutingKey(ByteBuffer routingKey) { - this.routingKey = routingKey; - return this; - } - - /** - * Sets the routing key for this bound statement, when the query partition key is composite and - * the routing key must be built from multiple values. - * - *

This is useful when the routing key can neither be set on the {@code PreparedStatement} this - * bound statement was built from, nor automatically computed from bound variables. In particular, - * this is the case if the partition key is composite and only some of its components are bound. - * - * @param routingKeyComponents the raw (binary) values to compose to obtain the routing key. - * @return this {@code BoundStatement} object. - * @see BoundStatement#getRoutingKey - */ - public BoundStatement setRoutingKey(ByteBuffer... routingKeyComponents) { - this.routingKey = SimpleStatement.compose(routingKeyComponents); - return this; - } - - /** {@inheritDoc} */ - @Override - public String getKeyspace() { - ColumnDefinitions defs = statement.getPreparedId().boundValuesMetadata.variables; - return defs.size() == 0 ? null : defs.getKeyspace(0); - } - - /** {@inheritDoc} */ - @Override - public int requestSizeInBytes(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - int size = Header.lengthFor(protocolVersion); - try { - size += - CBUtil.sizeOfShortBytes(preparedStatement().getPreparedId().boundValuesMetadata.id.bytes); - if (ProtocolFeature.PREPARED_METADATA_CHANGES.isSupportedBy(protocolVersion)) { - size += - CBUtil.sizeOfShortBytes(preparedStatement().getPreparedId().resultSetMetadata.id.bytes); - } - switch (protocolVersion) { - case V1: - size += CBUtil.sizeOfConsistencyLevel(getConsistencyLevel()); - break; - case V2: - case V3: - case V4: - case V5: - case V6: - size += CBUtil.sizeOfConsistencyLevel(getConsistencyLevel()); - size += QueryFlag.serializedSize(protocolVersion); - if (wrapper.values.length > 0) { - size += CBUtil.sizeOfValueList(wrapper.values); - } - // Fetch size, serial CL and default timestamp also depend on session-level defaults - // (QueryOptions). - // We always count them to avoid having to inject QueryOptions here, at worst we - // overestimate by a - // few bytes. - size += 4; // fetch size - if (getPagingState() != null) { - size += CBUtil.sizeOfValue(getPagingState()); - } - size += CBUtil.sizeOfConsistencyLevel(getSerialConsistencyLevel()); - if (ProtocolFeature.CLIENT_TIMESTAMPS.isSupportedBy(protocolVersion)) { - size += 8; // timestamp - } - if (ProtocolFeature.CUSTOM_PAYLOADS.isSupportedBy(protocolVersion) - && getOutgoingPayload() != null) { - size += CBUtil.sizeOfBytesMap(getOutgoingPayload()); - } - break; - default: - throw protocolVersion.unsupported(); - } - } catch (Exception e) { - size = -1; - } - return size; - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setBool(int i, boolean v) { - return wrapper.setBool(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setBool(String name, boolean v) { - return wrapper.setBool(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setByte(int i, byte v) { - return wrapper.setByte(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setByte(String name, byte v) { - return wrapper.setByte(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setShort(int i, short v) { - return wrapper.setShort(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setShort(String name, short v) { - return wrapper.setShort(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setInt(int i, int v) { - return wrapper.setInt(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setInt(String name, int v) { - return wrapper.setInt(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setLong(int i, long v) { - return wrapper.setLong(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setLong(String name, long v) { - return wrapper.setLong(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setTimestamp(int i, Date v) { - return wrapper.setTimestamp(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setTimestamp(String name, Date v) { - return wrapper.setTimestamp(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setDate(int i, LocalDate v) { - return wrapper.setDate(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setDate(String name, LocalDate v) { - return wrapper.setDate(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setTime(int i, long v) { - return wrapper.setTime(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setTime(String name, long v) { - return wrapper.setTime(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setFloat(int i, float v) { - return wrapper.setFloat(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setFloat(String name, float v) { - return wrapper.setFloat(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setDouble(int i, double v) { - return wrapper.setDouble(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setDouble(String name, double v) { - return wrapper.setDouble(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setString(int i, String v) { - return wrapper.setString(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setString(String name, String v) { - return wrapper.setString(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setBytes(int i, ByteBuffer v) { - return wrapper.setBytes(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setBytes(String name, ByteBuffer v) { - return wrapper.setBytes(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setBytesUnsafe(int i, ByteBuffer v) { - return wrapper.setBytesUnsafe(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setBytesUnsafe(String name, ByteBuffer v) { - return wrapper.setBytesUnsafe(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setVarint(int i, BigInteger v) { - return wrapper.setVarint(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setVarint(String name, BigInteger v) { - return wrapper.setVarint(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setDecimal(int i, BigDecimal v) { - return wrapper.setDecimal(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setDecimal(String name, BigDecimal v) { - return wrapper.setDecimal(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setUUID(int i, UUID v) { - return wrapper.setUUID(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setUUID(String name, UUID v) { - return wrapper.setUUID(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setInet(int i, InetAddress v) { - return wrapper.setInet(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setInet(String name, InetAddress v) { - return wrapper.setInet(name, v); - } - - /** - * Sets the {@code i}th value to the provided {@link Token}. - * - *

{@link #setPartitionKeyToken(Token)} should generally be preferred if you have a single - * token variable. - * - * @param i the index of the variable to set. - * @param v the value to set. - * @return this BoundStatement. - * @throws IndexOutOfBoundsException if {@code i < 0 || i >= - * this.preparedStatement().variables().size()}. - * @throws InvalidTypeException if column {@code i} is not of the type of the token's value. - */ - public BoundStatement setToken(int i, Token v) { - return wrapper.setToken(i, v); - } - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided token. - * - *

{@link #setPartitionKeyToken(Token)} should generally be preferred if you have a single - * token variable. - * - *

If you have multiple token variables, use positional binding ({@link #setToken(int, Token)}, - * or named bind markers: - * - *

{@code
-   * PreparedStatement pst = session.prepare("SELECT * FROM my_table WHERE token(k) > :min AND token(k) <= :max");
-   * BoundStatement b = pst.bind().setToken("min", minToken).setToken("max", maxToken);
-   * }
- * - * @param name the name of the variable to set; if multiple variables {@code name} are prepared, - * all of them are set. - * @param v the value to set. - * @return this BoundStatement. - * @throws IllegalArgumentException if {@code name} is not a prepared variable, that is, if {@code - * !this.preparedStatement().variables().names().contains(name)}. - * @throws InvalidTypeException if (any occurrence of) {@code name} is not of the type of the - * token's value. - */ - public BoundStatement setToken(String name, Token v) { - return wrapper.setToken(name, v); - } - - /** - * Sets the value for (all occurrences of) variable "{@code partition key token}" to the provided - * token (this is the name generated by Cassandra for markers corresponding to a {@code - * token(...)} call). - * - *

This method is a shorthand for statements with a single token variable: - * - *

{@code
-   * Token token = ...
-   * PreparedStatement pst = session.prepare("SELECT * FROM my_table WHERE token(k) = ?");
-   * BoundStatement b = pst.bind().setPartitionKeyToken(token);
-   * }
- * - * If you have multiple token variables, use positional binding ({@link #setToken(int, Token)}, or - * named bind markers: - * - *
{@code
-   * PreparedStatement pst = session.prepare("SELECT * FROM my_table WHERE token(k) > :min AND token(k) <= :max");
-   * BoundStatement b = pst.bind().setToken("min", minToken).setToken("max", maxToken);
-   * }
- * - * @param v the value to set. - * @return this BoundStatement. - * @throws IllegalArgumentException if {@code name} is not a prepared variable, that is, if {@code - * !this.preparedStatement().variables().names().contains(name)}. - * @throws InvalidTypeException if (any occurrence of) {@code name} is not of the type of the - * token's value. - */ - public BoundStatement setPartitionKeyToken(Token v) { - return setToken("partition key token", v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setList(int i, List v) { - return wrapper.setList(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setList(int i, List v, Class elementsClass) { - return wrapper.setList(i, v, elementsClass); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setList(int i, List v, TypeToken elementsType) { - return wrapper.setList(i, v, elementsType); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setList(String name, List v) { - return wrapper.setList(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setList(String name, List v, Class elementsClass) { - return wrapper.setList(name, v, elementsClass); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setList(String name, List v, TypeToken elementsType) { - return wrapper.setList(name, v, elementsType); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setMap(int i, Map v) { - return wrapper.setMap(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setMap( - int i, Map v, Class keysClass, Class valuesClass) { - return wrapper.setMap(i, v, keysClass, valuesClass); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setMap( - int i, Map v, TypeToken keysType, TypeToken valuesType) { - return wrapper.setMap(i, v, keysType, valuesType); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setMap(String name, Map v) { - return wrapper.setMap(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setMap( - String name, Map v, Class keysClass, Class valuesClass) { - return wrapper.setMap(name, v, keysClass, valuesClass); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setMap( - String name, Map v, TypeToken keysType, TypeToken valuesType) { - return wrapper.setMap(name, v, keysType, valuesType); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setSet(int i, Set v) { - return wrapper.setSet(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setSet(int i, Set v, Class elementsClass) { - return wrapper.setSet(i, v, elementsClass); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setSet(int i, Set v, TypeToken elementsType) { - return wrapper.setSet(i, v, elementsType); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setSet(String name, Set v) { - return wrapper.setSet(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setSet(String name, Set v, Class elementsClass) { - return wrapper.setSet(name, v, elementsClass); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setSet(String name, Set v, TypeToken elementsType) { - return wrapper.setSet(name, v, elementsType); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setUDTValue(int i, UDTValue v) { - return wrapper.setUDTValue(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setUDTValue(String name, UDTValue v) { - return wrapper.setUDTValue(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setTupleValue(int i, TupleValue v) { - return wrapper.setTupleValue(i, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setTupleValue(String name, TupleValue v) { - return wrapper.setTupleValue(name, v); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement set(int i, V v, Class targetClass) { - return wrapper.set(i, v, targetClass); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement set(String name, V v, Class targetClass) { - return wrapper.set(name, v, targetClass); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement set(int i, V v, TypeToken targetType) { - return wrapper.set(i, v, targetType); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement set(String name, V v, TypeToken targetType) { - return wrapper.set(name, v, targetType); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement set(int i, V v, TypeCodec codec) { - return wrapper.set(i, v, codec); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement set(String name, V v, TypeCodec codec) { - return wrapper.set(name, v, codec); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setToNull(int i) { - return wrapper.setToNull(i); - } - - /** {@inheritDoc} */ - @Override - public BoundStatement setToNull(String name) { - return wrapper.setToNull(name); - } - - /** {@inheritDoc} */ - @Override - public boolean isNull(int i) { - return wrapper.isNull(i); - } - - /** {@inheritDoc} */ - @Override - public boolean isNull(String name) { - return wrapper.isNull(name); - } - - /** {@inheritDoc} */ - @Override - public boolean getBool(int i) { - return wrapper.getBool(i); - } - - /** {@inheritDoc} */ - @Override - public boolean getBool(String name) { - return wrapper.getBool(name); - } - - /** {@inheritDoc} */ - @Override - public byte getByte(int i) { - return wrapper.getByte(i); - } - - /** {@inheritDoc} */ - @Override - public byte getByte(String name) { - return wrapper.getByte(name); - } - - /** {@inheritDoc} */ - @Override - public short getShort(int i) { - return wrapper.getShort(i); - } - - /** {@inheritDoc} */ - @Override - public short getShort(String name) { - return wrapper.getShort(name); - } - - /** {@inheritDoc} */ - @Override - public int getInt(int i) { - return wrapper.getInt(i); - } - - /** {@inheritDoc} */ - @Override - public int getInt(String name) { - return wrapper.getInt(name); - } - - /** {@inheritDoc} */ - @Override - public long getLong(int i) { - return wrapper.getLong(i); - } - - /** {@inheritDoc} */ - @Override - public long getLong(String name) { - return wrapper.getLong(name); - } - - /** {@inheritDoc} */ - @Override - public Date getTimestamp(int i) { - return wrapper.getTimestamp(i); - } - - /** {@inheritDoc} */ - @Override - public Date getTimestamp(String name) { - return wrapper.getTimestamp(name); - } - - /** {@inheritDoc} */ - @Override - public LocalDate getDate(int i) { - return wrapper.getDate(i); - } - - /** {@inheritDoc} */ - @Override - public LocalDate getDate(String name) { - return wrapper.getDate(name); - } - - /** {@inheritDoc} */ - @Override - public long getTime(int i) { - return wrapper.getTime(i); - } - - /** {@inheritDoc} */ - @Override - public long getTime(String name) { - return wrapper.getTime(name); - } - - /** {@inheritDoc} */ - @Override - public float getFloat(int i) { - return wrapper.getFloat(i); - } - - /** {@inheritDoc} */ - @Override - public float getFloat(String name) { - return wrapper.getFloat(name); - } - - /** {@inheritDoc} */ - @Override - public double getDouble(int i) { - return wrapper.getDouble(i); - } - - /** {@inheritDoc} */ - @Override - public double getDouble(String name) { - return wrapper.getDouble(name); - } - - /** {@inheritDoc} */ - @Override - public ByteBuffer getBytesUnsafe(int i) { - return wrapper.getBytesUnsafe(i); - } - - /** {@inheritDoc} */ - @Override - public ByteBuffer getBytesUnsafe(String name) { - return wrapper.getBytesUnsafe(name); - } - - /** {@inheritDoc} */ - @Override - public ByteBuffer getBytes(int i) { - return wrapper.getBytes(i); - } - - /** {@inheritDoc} */ - @Override - public ByteBuffer getBytes(String name) { - return wrapper.getBytes(name); - } - - /** {@inheritDoc} */ - @Override - public String getString(int i) { - return wrapper.getString(i); - } - - /** {@inheritDoc} */ - @Override - public String getString(String name) { - return wrapper.getString(name); - } - - /** {@inheritDoc} */ - @Override - public BigInteger getVarint(int i) { - return wrapper.getVarint(i); - } - - /** {@inheritDoc} */ - @Override - public BigInteger getVarint(String name) { - return wrapper.getVarint(name); - } - - /** {@inheritDoc} */ - @Override - public BigDecimal getDecimal(int i) { - return wrapper.getDecimal(i); - } - - /** {@inheritDoc} */ - @Override - public BigDecimal getDecimal(String name) { - return wrapper.getDecimal(name); - } - - /** {@inheritDoc} */ - @Override - public UUID getUUID(int i) { - return wrapper.getUUID(i); - } - - /** {@inheritDoc} */ - @Override - public UUID getUUID(String name) { - return wrapper.getUUID(name); - } - - /** {@inheritDoc} */ - @Override - public InetAddress getInet(int i) { - return wrapper.getInet(i); - } - - /** {@inheritDoc} */ - @Override - public InetAddress getInet(String name) { - return wrapper.getInet(name); - } - - /** {@inheritDoc} */ - @Override - public List getList(int i, Class elementsClass) { - return wrapper.getList(i, elementsClass); - } - - /** {@inheritDoc} */ - @Override - public List getList(int i, TypeToken elementsType) { - return wrapper.getList(i, elementsType); - } - - /** {@inheritDoc} */ - @Override - public List getList(String name, Class elementsClass) { - return wrapper.getList(name, elementsClass); - } - - /** {@inheritDoc} */ - @Override - public List getList(String name, TypeToken elementsType) { - return wrapper.getList(name, elementsType); - } - - /** {@inheritDoc} */ - @Override - public Set getSet(int i, Class elementsClass) { - return wrapper.getSet(i, elementsClass); - } - - /** {@inheritDoc} */ - @Override - public Set getSet(int i, TypeToken elementsType) { - return wrapper.getSet(i, elementsType); - } - - /** {@inheritDoc} */ - @Override - public Set getSet(String name, Class elementsClass) { - return wrapper.getSet(name, elementsClass); - } - - /** {@inheritDoc} */ - @Override - public Set getSet(String name, TypeToken elementsType) { - return wrapper.getSet(name, elementsType); - } - - /** {@inheritDoc} */ - @Override - public Map getMap(int i, Class keysClass, Class valuesClass) { - return wrapper.getMap(i, keysClass, valuesClass); - } - - /** {@inheritDoc} */ - @Override - public Map getMap(int i, TypeToken keysType, TypeToken valuesType) { - return wrapper.getMap(i, keysType, valuesType); - } - - /** {@inheritDoc} */ - @Override - public Map getMap(String name, Class keysClass, Class valuesClass) { - return wrapper.getMap(name, keysClass, valuesClass); - } - - /** {@inheritDoc} */ - @Override - public Map getMap(String name, TypeToken keysType, TypeToken valuesType) { - return wrapper.getMap(name, keysType, valuesType); - } - - /** {@inheritDoc} */ - @Override - public UDTValue getUDTValue(int i) { - return wrapper.getUDTValue(i); - } - - /** {@inheritDoc} */ - @Override - public UDTValue getUDTValue(String name) { - return wrapper.getUDTValue(name); - } - - /** {@inheritDoc} */ - @Override - public TupleValue getTupleValue(int i) { - return wrapper.getTupleValue(i); - } - - /** {@inheritDoc} */ - @Override - public TupleValue getTupleValue(String name) { - return wrapper.getTupleValue(name); - } - - /** {@inheritDoc} */ - @Override - public Object getObject(int i) { - return wrapper.getObject(i); - } - - /** {@inheritDoc} */ - @Override - public Object getObject(String name) { - return wrapper.getObject(name); - } - - /** {@inheritDoc} */ - @Override - public T get(int i, Class targetClass) { - return wrapper.get(i, targetClass); - } - - /** {@inheritDoc} */ - @Override - public T get(String name, Class targetClass) { - return wrapper.get(name, targetClass); - } - - /** {@inheritDoc} */ - @Override - public T get(int i, TypeToken targetType) { - return wrapper.get(i, targetType); - } - - /** {@inheritDoc} */ - @Override - public T get(String name, TypeToken targetType) { - return wrapper.get(name, targetType); - } - - /** {@inheritDoc} */ - @Override - public T get(int i, TypeCodec codec) { - return wrapper.get(i, codec); - } - - /** {@inheritDoc} */ - @Override - public T get(String name, TypeCodec codec) { - return wrapper.get(name, codec); - } - - void ensureAllSet() { - int index = 0; - for (ByteBuffer value : wrapper.values) { - if (value == BoundStatement.UNSET) - throw new IllegalStateException( - "Unset value at index " - + index - + ". " - + "If you want this value to be null, please set it to null explicitly."); - index += 1; - } - } - - static class DataWrapper extends AbstractData { - - DataWrapper(BoundStatement wrapped, int size) { - super(wrapped.statement.getPreparedId().protocolVersion, wrapped, size); - } - - protected int[] getAllIndexesOf(String name) { - return wrapped.statement.getVariables().getAllIdx(name); - } - - protected DataType getType(int i) { - return wrapped.statement.getVariables().getType(i); - } - - protected String getName(int i) { - return wrapped.statement.getVariables().getName(i); - } - - @Override - protected CodecRegistry getCodecRegistry() { - return wrapped.codecRegistry; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/BytesToSegmentDecoder.java b/driver-core/src/main/java/com/datastax/driver/core/BytesToSegmentDecoder.java deleted file mode 100644 index 58eda4fde27..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/BytesToSegmentDecoder.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.LengthFieldBasedFrameDecoder; -import java.nio.ByteOrder; - -/** - * Decodes {@link Segment}s from a stream of bytes. - * - *

This works like a regular length-field-based decoder, but we override {@link - * #getUnadjustedFrameLength} to handle two peculiarities: the length is encoded on 17 bits, and we - * also want to check the header CRC before we use it. So we parse the whole segment header ahead of - * time, and store it until we're ready to build the segment. - */ -class BytesToSegmentDecoder extends LengthFieldBasedFrameDecoder { - - private final SegmentCodec segmentCodec; - private SegmentCodec.Header header; - - BytesToSegmentDecoder(SegmentCodec segmentCodec) { - super( - // max length (Netty wants this to be the overall length including everything): - segmentCodec.headerLength() - + SegmentCodec.CRC24_LENGTH - + Segment.MAX_PAYLOAD_LENGTH - + SegmentCodec.CRC32_LENGTH, - // offset and size of the "length" field: that's the whole header - 0, - segmentCodec.headerLength() + SegmentCodec.CRC24_LENGTH, - // length adjustment: add the trailing CRC to the declared length - SegmentCodec.CRC32_LENGTH, - // bytes to skip: the header (we've already parsed it while reading the length) - segmentCodec.headerLength() + SegmentCodec.CRC24_LENGTH); - this.segmentCodec = segmentCodec; - } - - @Override - protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { - try { - ByteBuf payloadAndCrc = (ByteBuf) super.decode(ctx, in); - if (payloadAndCrc == null) { - return null; - } else { - assert header != null; - Segment segment = segmentCodec.decode(header, payloadAndCrc); - header = null; - return segment; - } - } catch (Exception e) { - // Don't hold on to a stale header if we failed to decode the rest of the segment - header = null; - throw e; - } - } - - @Override - protected long getUnadjustedFrameLength(ByteBuf buffer, int offset, int length, ByteOrder order) { - // The parent class calls this repeatedly for the same "frame" if there weren't enough - // accumulated bytes the first time. Only decode the header the first time: - if (header == null) { - header = segmentCodec.decodeHeader(buffer.slice(offset, length)); - } - return header.payloadLength; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/CBUtil.java b/driver-core/src/main/java/com/datastax/driver/core/CBUtil.java deleted file mode 100644 index 6941055f263..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/CBUtil.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.google.common.collect.ImmutableMap; -import io.netty.buffer.ByteBuf; -import io.netty.util.CharsetUtil; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; -import java.nio.charset.CharacterCodingException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -/** ByteBuf utility methods. */ - -// Implementation note: in order to facilitate loop optimizations by the JIT compiler, this class -// favors indexed loops over "foreach" loops. -@SuppressWarnings("ForLoopReplaceableByForEach") -abstract class CBUtil { // TODO rename - - private CBUtil() {} - - private static String readString(ByteBuf cb, int length) { - try { - String str = cb.toString(cb.readerIndex(), length, CharsetUtil.UTF_8); - cb.readerIndex(cb.readerIndex() + length); - return str; - } catch (IllegalStateException e) { - // That's the way netty encapsulate a CCE - if (e.getCause() instanceof CharacterCodingException) - throw new DriverInternalError("Cannot decode string as UTF8"); - else throw e; - } - } - - static String readString(ByteBuf cb) { - try { - int length = cb.readUnsignedShort(); - return readString(cb, length); - } catch (IndexOutOfBoundsException e) { - throw new DriverInternalError( - "Not enough bytes to read an UTF8 serialized string preceded by it's 2 bytes length"); - } - } - - private static void writeString(String str, ByteBuf cb) { - byte[] bytes = str.getBytes(CharsetUtil.UTF_8); - cb.writeShort(bytes.length); - cb.writeBytes(bytes); - } - - static int sizeOfString(String str) { - return 2 + encodedUTF8Length(str); - } - - private static int encodedUTF8Length(String st) { - int strlen = st.length(); - int utflen = 0; - for (int i = 0; i < strlen; i++) { - int c = st.charAt(i); - if ((c >= 0x0001) && (c <= 0x007F)) utflen++; - else if (c > 0x07FF) utflen += 3; - else utflen += 2; - } - return utflen; - } - - static void writeLongString(String str, ByteBuf cb) { - byte[] bytes = str.getBytes(CharsetUtil.UTF_8); - cb.writeInt(bytes.length); - cb.writeBytes(bytes); - } - - static int sizeOfLongString(String str) { - return 4 + str.getBytes(CharsetUtil.UTF_8).length; - } - - static byte[] readBytes(ByteBuf cb) { - try { - int length = cb.readUnsignedShort(); - byte[] bytes = new byte[length]; - cb.readBytes(bytes); - return bytes; - } catch (IndexOutOfBoundsException e) { - throw new DriverInternalError( - "Not enough bytes to read a byte array preceded by it's 2 bytes length"); - } - } - - static void writeShortBytes(byte[] bytes, ByteBuf cb) { - cb.writeShort(bytes.length); - cb.writeBytes(bytes); - } - - static int sizeOfShortBytes(byte[] bytes) { - return 2 + bytes.length; - } - - private static int sizeOfBytes(ByteBuffer bytes) { - return 4 + bytes.remaining(); - } - - static Map readBytesMap(ByteBuf cb) { - int length = cb.readUnsignedShort(); - ImmutableMap.Builder builder = ImmutableMap.builder(); - for (int i = 0; i < length; i++) { - String key = readString(cb); - ByteBuffer value = readValue(cb); - if (value == null) value = Statement.NULL_PAYLOAD_VALUE; - builder.put(key, value); - } - return builder.build(); - } - - static void writeBytesMap(Map m, ByteBuf cb) { - cb.writeShort(m.size()); - for (Map.Entry entry : m.entrySet()) { - writeString(entry.getKey(), cb); - ByteBuffer value = entry.getValue(); - if (value == Statement.NULL_PAYLOAD_VALUE) value = null; - writeValue(value, cb); - } - } - - static int sizeOfBytesMap(Map m) { - int size = 2; - for (Map.Entry entry : m.entrySet()) { - size += sizeOfString(entry.getKey()); - size += sizeOfBytes(entry.getValue()); - } - return size; - } - - static ConsistencyLevel readConsistencyLevel(ByteBuf cb) { - return ConsistencyLevel.fromCode(cb.readUnsignedShort()); - } - - static void writeConsistencyLevel(ConsistencyLevel consistency, ByteBuf cb) { - cb.writeShort(consistency.code); - } - - static int sizeOfConsistencyLevel(@SuppressWarnings("unused") ConsistencyLevel consistency) { - return 2; - } - - static > T readEnumValue(Class enumType, ByteBuf cb) { - String value = CBUtil.readString(cb); - try { - return Enum.valueOf(enumType, value.toUpperCase()); - } catch (IllegalArgumentException e) { - throw new DriverInternalError( - String.format("Invalid value '%s' for %s", value, enumType.getSimpleName())); - } - } - - static > void writeEnumValue(T enumValue, ByteBuf cb) { - writeString(enumValue.toString(), cb); - } - - static > int sizeOfEnumValue(T enumValue) { - return sizeOfString(enumValue.toString()); - } - - static UUID readUUID(ByteBuf cb) { - long msb = cb.readLong(); - long lsb = cb.readLong(); - return new UUID(msb, lsb); - } - - static List readStringList(ByteBuf cb) { - int length = cb.readUnsignedShort(); - List l = new ArrayList(length); - for (int i = 0; i < length; i++) { - l.add(readString(cb)); - } - return l; - } - - static void writeStringMap(Map m, ByteBuf cb) { - cb.writeShort(m.size()); - for (Map.Entry entry : m.entrySet()) { - writeString(entry.getKey(), cb); - writeString(entry.getValue(), cb); - } - } - - static int sizeOfStringMap(Map m) { - int size = 2; - for (Map.Entry entry : m.entrySet()) { - size += sizeOfString(entry.getKey()); - size += sizeOfString(entry.getValue()); - } - return size; - } - - static Map> readStringToStringListMap(ByteBuf cb) { - int length = cb.readUnsignedShort(); - Map> m = new HashMap>(length); - for (int i = 0; i < length; i++) { - String k = readString(cb).toUpperCase(); - List v = readStringList(cb); - m.put(k, v); - } - return m; - } - - static ByteBuffer readValue(ByteBuf cb) { - int length = cb.readInt(); - if (length < 0) return null; - ByteBuf slice = cb.readSlice(length); - - return ByteBuffer.wrap(readRawBytes(slice)); - } - - static void writeValue(byte[] bytes, ByteBuf cb) { - if (bytes == null) { - cb.writeInt(-1); - return; - } - - cb.writeInt(bytes.length); - cb.writeBytes(bytes); - } - - static void writeValue(ByteBuffer bytes, ByteBuf cb) { - if (bytes == null) { - cb.writeInt(-1); - return; - } - - if (bytes == BoundStatement.UNSET) { - cb.writeInt(-2); - return; - } - - cb.writeInt(bytes.remaining()); - cb.writeBytes(bytes.duplicate()); - } - - static int sizeOfValue(byte[] bytes) { - return 4 + (bytes == null ? 0 : bytes.length); - } - - static int sizeOfValue(ByteBuffer bytes) { - return 4 + (bytes == null ? 0 : bytes.remaining()); - } - - static void writeValueList(ByteBuffer[] values, ByteBuf cb) { - cb.writeShort(values.length); - for (int i = 0; i < values.length; i++) { - ByteBuffer value = values[i]; - CBUtil.writeValue(value, cb); - } - } - - static int sizeOfValueList(ByteBuffer[] values) { - int size = 2; - for (int i = 0; i < values.length; i++) { - ByteBuffer value = values[i]; - size += CBUtil.sizeOfValue(value); - } - return size; - } - - static void writeNamedValueList(Map namedValues, ByteBuf cb) { - cb.writeShort(namedValues.size()); - for (Map.Entry entry : namedValues.entrySet()) { - CBUtil.writeString(entry.getKey(), cb); - CBUtil.writeValue(entry.getValue(), cb); - } - } - - static int sizeOfNamedValueList(Map namedValues) { - int size = 2; - for (Map.Entry entry : namedValues.entrySet()) { - size += CBUtil.sizeOfString(entry.getKey()); - size += CBUtil.sizeOfValue(entry.getValue()); - } - return size; - } - - static InetSocketAddress readInet(ByteBuf cb) { - int addrSize = cb.readByte() & 0xFF; - byte[] address = new byte[addrSize]; - cb.readBytes(address); - int port = cb.readInt(); - try { - return new InetSocketAddress(InetAddress.getByAddress(address), port); - } catch (UnknownHostException e) { - throw new DriverInternalError( - String.format( - "Invalid IP address (%d.%d.%d.%d) while deserializing inet address", - address[0], address[1], address[2], address[3])); - } - } - - static InetAddress readInetWithoutPort(ByteBuf cb) { - int addrSize = cb.readByte() & 0xFF; - byte[] address = new byte[addrSize]; - cb.readBytes(address); - try { - return InetAddress.getByAddress(address); - } catch (UnknownHostException e) { - throw new DriverInternalError( - String.format( - "Invalid IP address (%d.%d.%d.%d) while deserializing inet address", - address[0], address[1], address[2], address[3])); - } - } - - /* - * Reads *all* readable bytes from {@code cb} and return them. - * If {@code cb} is backed by an array, this will return the underlying array directly, without copy. - */ - private static byte[] readRawBytes(ByteBuf cb) { - if (cb.hasArray() && cb.readableBytes() == cb.array().length) { - // Move the readerIndex just so we consistently consume the input - cb.readerIndex(cb.writerIndex()); - return cb.array(); - } - - // Otherwise, just read the bytes in a new array - byte[] bytes = new byte[cb.readableBytes()]; - cb.readBytes(bytes); - return bytes; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/CancelledSpeculativeExecutionException.java b/driver-core/src/main/java/com/datastax/driver/core/CancelledSpeculativeExecutionException.java deleted file mode 100644 index c92ff7e8efd..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/CancelledSpeculativeExecutionException.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Special exception that gets emitted to {@link LatencyTracker}s with the latencies of cancelled - * speculative executions. This allows those trackers to choose whether to ignore those latencies or - * not. - */ -class CancelledSpeculativeExecutionException extends Exception { - - static CancelledSpeculativeExecutionException INSTANCE = - new CancelledSpeculativeExecutionException(); - - private CancelledSpeculativeExecutionException() { - super(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ChainedResultSetFuture.java b/driver-core/src/main/java/com/datastax/driver/core/ChainedResultSetFuture.java deleted file mode 100644 index 47d7767d332..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ChainedResultSetFuture.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.util.concurrent.AbstractFuture; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Uninterruptibles; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -/** A {@code ResultSetFuture} that will complete when its source future completes. */ -class ChainedResultSetFuture extends AbstractFuture implements ResultSetFuture { - - private volatile ResultSetFuture source; - - void setSource(ResultSetFuture source) { - if (this.isCancelled()) source.cancel(false); - this.source = source; - GuavaCompatibility.INSTANCE.addCallback( - source, - new FutureCallback() { - @Override - public void onSuccess(ResultSet result) { - ChainedResultSetFuture.this.set(result); - } - - @Override - public void onFailure(Throwable t) { - ChainedResultSetFuture.this.setException(t); - } - }); - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return (source == null || source.cancel(mayInterruptIfRunning)) - && super.cancel(mayInterruptIfRunning); - } - - @Override - public ResultSet getUninterruptibly() { - try { - return Uninterruptibles.getUninterruptibly(this); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } - - @Override - public ResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException { - try { - return Uninterruptibles.getUninterruptibly(this, timeout, unit); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Clock.java b/driver-core/src/main/java/com/datastax/driver/core/Clock.java deleted file mode 100644 index b9bdc27547d..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Clock.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.NANOSECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; - -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A small abstraction around system clock that aims to provide microsecond precision with the best - * accuracy possible. - */ -interface Clock { - - /** - * Returns the current time in microseconds. - * - * @return the difference, measured in microseconds, between the current time and and the Epoch - * (that is, midnight, January 1, 1970 UTC). - */ - long currentTimeMicros(); -} - -/** - * Factory that returns the best Clock implementation depending on what native libraries are - * available in the system. If LibC is available through JNR, and if the system property {@code - * com.datastax.driver.USE_NATIVE_CLOCK} is set to {@code true} (which is the default value), then - * {@link NativeClock} is returned, otherwise {@link SystemClock} is returned. - */ -class ClockFactory { - - private static final Logger LOGGER = LoggerFactory.getLogger(ClockFactory.class); - - private static final String USE_NATIVE_CLOCK_SYSTEM_PROPERTY = - "com.datastax.driver.USE_NATIVE_CLOCK"; - - static Clock newInstance() { - if (SystemProperties.getBoolean(USE_NATIVE_CLOCK_SYSTEM_PROPERTY, true) - && Native.isGettimeofdayAvailable()) { - LOGGER.info("Using native clock to generate timestamps."); - return new NativeClock(); - } else { - LOGGER.info("Using java.lang.System clock to generate timestamps."); - return new SystemClock(); - } - } -} - -/** - * Default implementation of a clock that delegates its calls to the system clock. - * - * @see System#currentTimeMillis() - */ -class SystemClock implements Clock { - - @Override - public long currentTimeMicros() { - return System.currentTimeMillis() * 1000; - } -} - -/** - * Provides the current time with microseconds precision with some reasonable accuracy through the - * use of {@link Native#currentTimeMicros()}. - * - *

Because calling JNR methods is slightly expensive, we only call it once per second and add the - * number of nanoseconds since the last call to get the current time, which is good enough an - * accuracy for our purpose (see CASSANDRA-6106). - * - *

This reduces the cost of the call to {@link NativeClock#currentTimeMicros()} to levels - * comparable to those of a call to {@link System#nanoTime()}. - */ -class NativeClock implements Clock { - - private static final long ONE_SECOND_NS = NANOSECONDS.convert(1, SECONDS); - private static final long ONE_MILLISECOND_NS = NANOSECONDS.convert(1, MILLISECONDS); - - /** - * Records a time in micros along with the System.nanoTime() value at the time the time is - * fetched. - */ - private static class FetchedTime { - - private final long timeInMicros; - private final long nanoTimeAtCheck; - - private FetchedTime(long timeInMicros, long nanoTimeAtCheck) { - this.timeInMicros = timeInMicros; - this.nanoTimeAtCheck = nanoTimeAtCheck; - } - } - - private final AtomicReference lastFetchedTime = - new AtomicReference(fetchTimeMicros()); - - @Override - public long currentTimeMicros() { - FetchedTime spec = lastFetchedTime.get(); - long curNano = System.nanoTime(); - if (curNano > spec.nanoTimeAtCheck + ONE_SECOND_NS) { - lastFetchedTime.compareAndSet(spec, spec = fetchTimeMicros()); - } - return spec.timeInMicros + ((curNano - spec.nanoTimeAtCheck) / 1000); - } - - private static FetchedTime fetchTimeMicros() { - // To compensate for the fact that the Native.currentTimeMicros call could take - // some time, instead of picking the nano time before the call or after the - // call, we take the average of both. - long start = System.nanoTime(); - long micros = Native.currentTimeMicros(); - long end = System.nanoTime(); - // If it turns out the call took us more than 1 millisecond (can happen while - // the JVM warms up, unlikely otherwise, but no reasons to take risks), fall back - // to System.currentTimeMillis() temporarily - if ((end - start) > ONE_MILLISECOND_NS) - return new FetchedTime(System.currentTimeMillis() * 1000, System.nanoTime()); - return new FetchedTime(micros, (end + start) / 2); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/CloseFuture.java b/driver-core/src/main/java/com/datastax/driver/core/CloseFuture.java deleted file mode 100644 index 9a6d4b5084e..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/CloseFuture.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.util.concurrent.AbstractFuture; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import java.util.List; - -/** - * A future on the shutdown of a Cluster or Session instance. - * - *

This is a standard future except for the fact that this class has an additional {@link #force} - * method that can be used to expedite the shutdown process (see below). - * - *

Note that this class implements Guava's {@code - * ListenableFuture} and can so be used with Guava's future utilities. - */ -public abstract class CloseFuture extends AbstractFuture { - - CloseFuture() {} - - static CloseFuture immediateFuture() { - CloseFuture future = - new CloseFuture() { - @Override - public CloseFuture force() { - return this; - } - }; - future.set(null); - return future; - } - - /** - * Try to force the completion of the shutdown this is a future of. - * - *

This method will do its best to expedite the shutdown process. In particular, all - * connections will be closed right away, even if there are ongoing queries at the time this - * method is called. - * - *

Note that this method does not block. The completion of this method does not imply the - * shutdown process is done, you still need to wait on this future to ensure that, but calling - * this method will ensure said future will return in a timely way. - * - * @return this {@code CloseFuture}. - */ - public abstract CloseFuture force(); - - // Internal utility for cases where we want to build a future that wait on other ones - static class Forwarding extends CloseFuture { - - private final List futures; - - Forwarding(List futures) { - this.futures = futures; - - GuavaCompatibility.INSTANCE.addCallback( - Futures.allAsList(futures), - new FutureCallback>() { - @Override - public void onFailure(Throwable t) { - Forwarding.this.setException(t); - } - - @Override - public void onSuccess(List v) { - Forwarding.this.onFuturesDone(); - } - }); - } - - @Override - public CloseFuture force() { - for (CloseFuture future : futures) future.force(); - return this; - } - - protected void onFuturesDone() { - set(null); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/CloudConfig.java b/driver-core/src/main/java/com/datastax/driver/core/CloudConfig.java deleted file mode 100644 index 7f9c3878c18..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/CloudConfig.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.collect.ImmutableList; -import java.net.InetSocketAddress; -import java.util.List; - -class CloudConfig { - - private final InetSocketAddress proxyAddress; - private final List endPoints; - private final String localDatacenter; - private final SSLOptions sslOptions; - private final AuthProvider authProvider; - - CloudConfig( - InetSocketAddress proxyAddress, - List endPoints, - String localDatacenter, - SSLOptions sslOptions, - AuthProvider authProvider) { - this.proxyAddress = proxyAddress; - this.endPoints = ImmutableList.copyOf(endPoints); - this.localDatacenter = localDatacenter; - this.sslOptions = sslOptions; - this.authProvider = authProvider; - } - - /** @return not null proxy Address */ - InetSocketAddress getProxyAddress() { - return proxyAddress; - } - - /** @return not null endpoints */ - List getEndPoints() { - return endPoints; - } - - /** @return not null local data center */ - String getLocalDatacenter() { - return localDatacenter; - } - - /** @return not null ssl options that can be used to connect to SniProxy */ - SSLOptions getSslOptions() { - return sslOptions; - } - - /** - * @return nullable AuthProvider that can be used to connect to proxy or null if there was not - * username/password provided in the secure bundle - */ - AuthProvider getAuthProvider() { - return authProvider; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/CloudConfigFactory.java b/driver-core/src/main/java/com/datastax/driver/core/CloudConfigFactory.java deleted file mode 100644 index f11b1fcecf4..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/CloudConfigFactory.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.google.common.base.Charsets.UTF_8; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Throwables; -import com.google.common.io.ByteStreams; -import com.google.common.net.HostAndPort; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManagerFactory; - -class CloudConfigFactory { - - /** - * Creates a {@link CloudConfig} with information fetched from the specified {@link InputStream}. - * - *

The stream must contain a valid secure connect bundle archive in ZIP format. Note that the - * stream will be closed after a call to that method and cannot be used anymore. - * - * @param cloudConfig the stream to read the Cloud configuration from; cannot be null. - * @throws IOException If the Cloud configuration cannot be read. - * @throws GeneralSecurityException If the Cloud SSL context cannot be created. - */ - CloudConfig createCloudConfig(InputStream cloudConfig) - throws IOException, GeneralSecurityException { - checkNotNull(cloudConfig, "cloudConfig cannot be null"); - JsonNode configJson = null; - ByteArrayOutputStream keyStoreOutputStream = null; - ByteArrayOutputStream trustStoreOutputStream = null; - ObjectMapper mapper = new ObjectMapper().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); - ZipInputStream zipInputStream = null; - try { - zipInputStream = new ZipInputStream(cloudConfig); - ZipEntry entry; - while ((entry = zipInputStream.getNextEntry()) != null) { - String fileName = entry.getName(); - if (fileName.equals("config.json")) { - configJson = mapper.readTree(zipInputStream); - } else if (fileName.equals("identity.jks")) { - keyStoreOutputStream = new ByteArrayOutputStream(); - ByteStreams.copy(zipInputStream, keyStoreOutputStream); - } else if (fileName.equals("trustStore.jks")) { - trustStoreOutputStream = new ByteArrayOutputStream(); - ByteStreams.copy(zipInputStream, trustStoreOutputStream); - } - } - } finally { - if (zipInputStream != null) { - zipInputStream.close(); - } - } - - if (configJson == null) { - throw new IllegalStateException("Invalid bundle: missing file config.json"); - } - if (keyStoreOutputStream == null) { - throw new IllegalStateException("Invalid bundle: missing file identity.jks"); - } - if (trustStoreOutputStream == null) { - throw new IllegalStateException("Invalid bundle: missing file trustStore.jks"); - } - char[] keyStorePassword = getKeyStorePassword(configJson); - char[] trustStorePassword = getTrustStorePassword(configJson); - ByteArrayInputStream keyStoreInputStream = - new ByteArrayInputStream(keyStoreOutputStream.toByteArray()); - ByteArrayInputStream trustStoreInputStream = - new ByteArrayInputStream(trustStoreOutputStream.toByteArray()); - SSLContext sslContext = - createSslContext( - keyStoreInputStream, keyStorePassword, trustStoreInputStream, trustStorePassword); - URL metadataServiceUrl = getMetadataServiceUrl(configJson); - JsonNode proxyMetadataJson; - BufferedReader proxyMetadata = null; - try { - proxyMetadata = fetchProxyMetadata(metadataServiceUrl, sslContext); - proxyMetadataJson = mapper.readTree(proxyMetadata); - } finally { - if (proxyMetadata != null) { - proxyMetadata.close(); - } - } - InetSocketAddress sniProxyAddress = getSniProxyAddress(proxyMetadataJson); - List endPoints = getEndPoints(proxyMetadataJson, sniProxyAddress); - String localDatacenter = getLocalDatacenter(proxyMetadataJson); - SSLOptions sslOptions = getSSLOptions(sslContext); - AuthProvider authProvider = getAuthProvider(configJson); - return new CloudConfig(sniProxyAddress, endPoints, localDatacenter, sslOptions, authProvider); - } - - protected char[] getKeyStorePassword(JsonNode configFile) { - if (configFile.has("keyStorePassword")) { - return configFile.get("keyStorePassword").asText().toCharArray(); - } else { - throw new IllegalStateException("Invalid config.json: missing field keyStorePassword"); - } - } - - protected char[] getTrustStorePassword(JsonNode configFile) { - if (configFile.has("trustStorePassword")) { - return configFile.get("trustStorePassword").asText().toCharArray(); - } else { - throw new IllegalStateException("Invalid config.json: missing field trustStorePassword"); - } - } - - protected URL getMetadataServiceUrl(JsonNode configFile) throws MalformedURLException { - if (configFile.has("host")) { - String metadataServiceHost = configFile.get("host").asText(); - if (configFile.has("port")) { - int metadataServicePort = configFile.get("port").asInt(); - return new URL("https", metadataServiceHost, metadataServicePort, "/metadata"); - } else { - throw new IllegalStateException("Invalid config.json: missing field port"); - } - } else { - throw new IllegalStateException("Invalid config.json: missing field host"); - } - } - - protected AuthProvider getAuthProvider(JsonNode configFile) { - if (configFile.has("username")) { - String username = configFile.get("username").asText(); - if (configFile.has("password")) { - String password = configFile.get("password").asText(); - return new PlainTextAuthProvider(username, password); - } - } - return null; - } - - protected SSLContext createSslContext( - ByteArrayInputStream keyStoreInputStream, - char[] keyStorePassword, - ByteArrayInputStream trustStoreInputStream, - char[] trustStorePassword) - throws IOException, GeneralSecurityException { - KeyManagerFactory kmf = createKeyManagerFactory(keyStoreInputStream, keyStorePassword); - TrustManagerFactory tmf = createTrustManagerFactory(trustStoreInputStream, trustStorePassword); - SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom()); - return sslContext; - } - - protected KeyManagerFactory createKeyManagerFactory( - InputStream keyStoreInputStream, char[] keyStorePassword) - throws IOException, GeneralSecurityException { - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(keyStoreInputStream, keyStorePassword); - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(ks, keyStorePassword); - Arrays.fill(keyStorePassword, (char) 0); - return kmf; - } - - protected TrustManagerFactory createTrustManagerFactory( - InputStream trustStoreInputStream, char[] trustStorePassword) - throws IOException, GeneralSecurityException { - KeyStore ts = KeyStore.getInstance("JKS"); - ts.load(trustStoreInputStream, trustStorePassword); - TrustManagerFactory tmf = - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(ts); - Arrays.fill(trustStorePassword, (char) 0); - return tmf; - } - - protected BufferedReader fetchProxyMetadata(URL metadataServiceUrl, SSLContext sslContext) - throws IOException { - HttpsURLConnection connection = (HttpsURLConnection) metadataServiceUrl.openConnection(); - connection.setSSLSocketFactory(sslContext.getSocketFactory()); - connection.setRequestMethod("GET"); - connection.setRequestProperty("host", "localhost"); - return new BufferedReader(new InputStreamReader(connection.getInputStream(), UTF_8)); - } - - protected String getLocalDatacenter(JsonNode proxyMetadata) { - JsonNode contactInfo = getContactInfo(proxyMetadata); - if (contactInfo.has("local_dc")) { - return contactInfo.get("local_dc").asText(); - } else { - throw new IllegalStateException("Invalid proxy metadata: missing field local_dc"); - } - } - - protected InetSocketAddress getSniProxyAddress(JsonNode proxyMetadata) { - JsonNode contactInfo = getContactInfo(proxyMetadata); - if (contactInfo.has("sni_proxy_address")) { - HostAndPort sniProxyHostAndPort = - HostAndPort.fromString(contactInfo.get("sni_proxy_address").asText()); - if (!sniProxyHostAndPort.hasPort()) { - throw new IllegalStateException( - "Invalid proxy metadata: missing port from field sni_proxy_address"); - } - String host = GuavaCompatibility.INSTANCE.getHost(sniProxyHostAndPort); - return InetSocketAddress.createUnresolved(host, sniProxyHostAndPort.getPort()); - } else { - throw new IllegalStateException("Invalid proxy metadata: missing field sni_proxy_address"); - } - } - - protected List getEndPoints(JsonNode proxyMetadata, InetSocketAddress sniProxyAddress) { - JsonNode contactInfo = getContactInfo(proxyMetadata); - if (contactInfo.has("contact_points")) { - List endPoints = new ArrayList(); - JsonNode hostIdsJson = contactInfo.get("contact_points"); - for (int i = 0; i < hostIdsJson.size(); i++) { - endPoints.add(new SniEndPoint(sniProxyAddress, hostIdsJson.get(i).asText())); - } - return endPoints; - } else { - throw new IllegalStateException("Invalid proxy metadata: missing field contact_points"); - } - } - - protected JsonNode getContactInfo(JsonNode proxyMetadata) { - if (proxyMetadata.has("contact_info")) { - return proxyMetadata.get("contact_info"); - } else { - throw new IllegalStateException("Invalid proxy metadata: missing field contact_info"); - } - } - - protected SSLOptions getSSLOptions(SSLContext sslContext) { - try { - return SniSSLOptions.builder().withSSLContext(sslContext).build(); - } catch (Exception e) { - throw Throwables.propagate(e); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Cluster.java b/driver-core/src/main/java/com/datastax/driver/core/Cluster.java deleted file mode 100644 index 0efaced9322..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Cluster.java +++ /dev/null @@ -1,3295 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.SchemaElement.KEYSPACE; - -import com.datastax.driver.core.exceptions.AuthenticationException; -import com.datastax.driver.core.exceptions.BusyConnectionException; -import com.datastax.driver.core.exceptions.ConnectionException; -import com.datastax.driver.core.exceptions.InvalidQueryException; -import com.datastax.driver.core.exceptions.NoHostAvailableException; -import com.datastax.driver.core.exceptions.SyntaxError; -import com.datastax.driver.core.exceptions.UnsupportedProtocolVersionException; -import com.datastax.driver.core.policies.AddressTranslator; -import com.datastax.driver.core.policies.IdentityTranslator; -import com.datastax.driver.core.policies.LatencyAwarePolicy; -import com.datastax.driver.core.policies.LoadBalancingPolicy; -import com.datastax.driver.core.policies.Policies; -import com.datastax.driver.core.policies.ReconnectionPolicy; -import com.datastax.driver.core.policies.RetryPolicy; -import com.datastax.driver.core.policies.SpeculativeExecutionPolicy; -import com.datastax.driver.core.utils.MoreFutures; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Functions; -import com.google.common.base.Predicates; -import com.google.common.base.Strings; -import com.google.common.base.Throwables; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.MapMaker; -import com.google.common.collect.SetMultimap; -import com.google.common.collect.Sets; -import com.google.common.util.concurrent.AsyncFunction; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.common.util.concurrent.SettableFuture; -import com.google.common.util.concurrent.Uninterruptibles; -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.UnknownHostException; -import java.security.GeneralSecurityException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.ResourceBundle; -import java.util.Set; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Information and known state of a Cassandra cluster. - * - *

This is the main entry point of the driver. A simple example of access to a Cassandra cluster - * would be: - * - *

- *   Cluster cluster = Cluster.builder().addContactPoint("192.168.0.1").build();
- *   Session session = cluster.connect("db1");
- *
- *   for (Row row : session.execute("SELECT * FROM table1"))
- *       // do something ...
- * 
- * - *

A cluster object maintains a permanent connection to one of the cluster nodes which it uses - * solely to maintain information on the state and current topology of the cluster. Using the - * connection, the driver will discover all the nodes currently in the cluster as well as new nodes - * joining the cluster subsequently. - */ -public class Cluster implements Closeable { - - private static final Logger logger = LoggerFactory.getLogger(Cluster.class); - - private static final ResourceBundle driverProperties = - ResourceBundle.getBundle("com.datastax.driver.core.Driver"); - - static { - logDriverVersion(); - // Force initialization to fail fast if there is an issue detecting the version - GuavaCompatibility.init(); - } - - @VisibleForTesting - static final int NEW_NODE_DELAY_SECONDS = - SystemProperties.getInt("com.datastax.driver.NEW_NODE_DELAY_SECONDS", 1); - - // Some per-JVM number that allows to generate unique cluster names when - // multiple Cluster instance are created in the same JVM. - private static final AtomicInteger CLUSTER_ID = new AtomicInteger(0); - - private static final int NOTIF_LOCK_TIMEOUT_SECONDS = - SystemProperties.getInt("com.datastax.driver.NOTIF_LOCK_TIMEOUT_SECONDS", 60); - - final Manager manager; - - /** - * Constructs a new Cluster instance. - * - *

This constructor is mainly exposed so Cluster can be sub-classed as a means to make - * testing/mocking easier or to "intercept" its method call. Most users shouldn't extend this - * class however and should prefer either using the {@link #builder} or calling {@link #buildFrom} - * with a custom Initializer. - * - * @param name the name to use for the cluster (this is not the Cassandra cluster name, see {@link - * #getClusterName}). - * @param contactPoints the list of contact points to use for the new cluster. - * @param configuration the configuration for the new cluster. - */ - protected Cluster(String name, List contactPoints, Configuration configuration) { - this(name, contactPoints, configuration, Collections.emptySet()); - } - - /** - * Constructs a new Cluster instance. - * - *

This constructor is mainly exposed so Cluster can be sub-classed as a means to make - * testing/mocking easier or to "intercept" its method call. Most users shouldn't extend this - * class however and should prefer using the {@link #builder}. - * - * @param initializer the initializer to use. - * @see #buildFrom - */ - protected Cluster(Initializer initializer) { - this( - initializer.getClusterName(), - checkNotEmpty(initializer.getContactPoints()), - initializer.getConfiguration(), - initializer.getInitialListeners()); - } - - private static List checkNotEmpty(List contactPoints) { - if (contactPoints.isEmpty()) - throw new IllegalArgumentException("Cannot build a cluster without contact points"); - return contactPoints; - } - - private Cluster( - String name, - List contactPoints, - Configuration configuration, - Collection listeners) { - this.manager = new Manager(name, contactPoints, configuration, listeners); - } - - /** - * Initialize this Cluster instance. - * - *

This method creates an initial connection to one of the contact points used to construct the - * {@code Cluster} instance. That connection is then used to populate the cluster {@link - * Metadata}. - * - *

Calling this method is optional in the sense that any call to one of the {@code connect} - * methods of this object will automatically trigger a call to this method beforehand. It is thus - * only useful to call this method if for some reason you want to populate the metadata (or test - * that at least one contact point can be reached) without creating a first {@code Session}. - * - *

Please note that this method only creates one control connection for gathering cluster - * metadata. In particular, it doesn't create any connection pools. Those are created when a new - * {@code Session} is created through {@code connect}. - * - *

This method has no effect if the cluster is already initialized. - * - * @return this {@code Cluster} object. - * @throws NoHostAvailableException if no host amongst the contact points can be reached. - * @throws AuthenticationException if an authentication error occurs while contacting the initial - * contact points. - * @throws IllegalStateException if the Cluster was closed prior to calling this method. This can - * occur either directly (through {@link #close()} or {@link #closeAsync()}), or as a result - * of an error while initializing the Cluster. - */ - public Cluster init() { - this.manager.init(); - return this; - } - - /** - * Build a new cluster based on the provided initializer. - * - *

Note that for building a cluster pragmatically, Cluster.Builder provides a slightly less - * verbose shortcut with {@link Builder#build}. - * - *

Also note that that all the contact points provided by {@code initializer} must share the - * same port. - * - * @param initializer the Cluster.Initializer to use - * @return the newly created Cluster instance - * @throws IllegalArgumentException if the list of contact points provided by {@code initializer} - * is empty or if not all those contact points have the same port. - */ - public static Cluster buildFrom(Initializer initializer) { - return new Cluster(initializer); - } - - /** - * Creates a new {@link Cluster.Builder} instance. - * - *

This is a convenience method for {@code new Cluster.Builder()}. - * - * @return the new cluster builder. - */ - public static Cluster.Builder builder() { - return new Cluster.Builder(); - } - - /** - * Returns the current version of the driver. - * - *

This is intended for products that wrap or extend the driver, as a way to check - * compatibility if end-users override the driver version in their application. - * - * @return the version. - */ - public static String getDriverVersion() { - return driverProperties.getString("driver.version"); - } - - /** - * Logs the driver version to the console. - * - *

This method logs the version using the logger {@code com.datastax.driver.core} and level - * {@code INFO}. - */ - public static void logDriverVersion() { - Logger core = LoggerFactory.getLogger("com.datastax.driver.core"); - core.info("DataStax Java driver {} for Apache Cassandra", getDriverVersion()); - } - - /** - * Creates a new session on this cluster but does not initialize it. - * - *

Because this method does not perform any initialization, it cannot fail. The initialization - * of the session (the connection of the Session to the Cassandra nodes) will occur if either the - * {@link Session#init} method is called explicitly, or whenever the returned session object is - * used. - * - *

Once a session returned by this method gets initialized (see above), it will be set to no - * keyspace. If you want to set such session to a keyspace, you will have to explicitly execute a - * 'USE mykeyspace' query. - * - *

Note that if you do not particularly need to defer initialization, it is simpler to use one - * of the {@code connect()} method of this class. - * - * @return a new, non-initialized session on this cluster. - */ - public Session newSession() { - checkNotClosed(manager); - return manager.newSession(); - } - - /** - * Creates a new session on this cluster and initialize it. - * - *

Note that this method will initialize the newly created session, trying to connect to the - * Cassandra nodes before returning. If you only want to create a Session object without - * initializing it right away, see {@link #newSession}. - * - * @return a new session on this cluster sets to no keyspace. - * @throws NoHostAvailableException if the Cluster has not been initialized yet ({@link #init} has - * not be called and this is the first connect call) and no host amongst the contact points - * can be reached. - * @throws AuthenticationException if an authentication error occurs while contacting the initial - * contact points. - * @throws IllegalStateException if the Cluster was closed prior to calling this method. This can - * occur either directly (through {@link #close()} or {@link #closeAsync()}), or as a result - * of an error while initializing the Cluster. - */ - public Session connect() { - try { - return Uninterruptibles.getUninterruptibly(connectAsync()); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } - - /** - * Creates a new session on this cluster, initialize it and sets the keyspace to the provided one. - * - *

Note that this method will initialize the newly created session, trying to connect to the - * Cassandra nodes before returning. If you only want to create a Session object without - * initializing it right away, see {@link #newSession}. - * - * @param keyspace The name of the keyspace to use for the created {@code Session}. - * @return a new session on this cluster sets to keyspace {@code keyspaceName}. - * @throws NoHostAvailableException if the Cluster has not been initialized yet ({@link #init} has - * not be called and this is the first connect call) and no host amongst the contact points - * can be reached, or if no host can be contacted to set the {@code keyspace}. - * @throws AuthenticationException if an authentication error occurs while contacting the initial - * contact points. - * @throws InvalidQueryException if the keyspace does not exist. - * @throws IllegalStateException if the Cluster was closed prior to calling this method. This can - * occur either directly (through {@link #close()} or {@link #closeAsync()}), or as a result - * of an error while initializing the Cluster. - */ - public Session connect(String keyspace) { - try { - return Uninterruptibles.getUninterruptibly(connectAsync(keyspace)); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } - - /** - * Creates a new session on this cluster and initializes it asynchronously. - * - *

This will also initialize the {@code Cluster} if needed; note that cluster initialization - * happens synchronously on the thread that called this method. Therefore it is recommended to - * initialize the cluster at application startup, and not rely on this method to do it. - * - *

Note that if a {@linkplain Configuration#getDefaultKeyspace() default keyspace} has been - * configured for use with a DBaaS cluster, this method will attempt to set the session keyspace - * to that keyspace, effectively behaving like {@link #connect(String)}. - * - * @return a future that will complete when the session is fully initialized. - * @throws NoHostAvailableException if the Cluster has not been initialized yet ({@link #init} has - * not been called and this is the first connect call) and no host amongst the contact points - * can be reached. - * @throws IllegalStateException if the Cluster was closed prior to calling this method. This can - * occur either directly (through {@link #close()} or {@link #closeAsync()}), or as a result - * of an error while initializing the Cluster. - * @see #connect() - */ - public ListenableFuture connectAsync() { - String defaultKeyspace = getConfiguration().getDefaultKeyspace(); - return connectAsync(defaultKeyspace); - } - - /** - * Creates a new session on this cluster, and initializes it to the given keyspace asynchronously. - * - *

This will also initialize the {@code Cluster} if needed; note that cluster initialization - * happens synchronously on the thread that called this method. Therefore it is recommended to - * initialize the cluster at application startup, and not rely on this method to do it. - * - * @param keyspace The name of the keyspace to use for the created {@code Session}. - * @return a future that will complete when the session is fully initialized. - * @throws NoHostAvailableException if the Cluster has not been initialized yet ({@link #init} has - * not been called and this is the first connect call) and no host amongst the contact points - * can be reached. - * @throws IllegalStateException if the Cluster was closed prior to calling this method. This can - * occur either directly (through {@link #close()} or {@link #closeAsync()}), or as a result - * of an error while initializing the Cluster. - */ - public ListenableFuture connectAsync(final String keyspace) { - checkNotClosed(manager); - init(); - final Session session = manager.newSession(); - ListenableFuture sessionInitialized = session.initAsync(); - if (keyspace == null) { - return sessionInitialized; - } else { - final String useQuery = "USE " + keyspace; - ListenableFuture keyspaceSet = - GuavaCompatibility.INSTANCE.transformAsync( - sessionInitialized, - new AsyncFunction() { - @Override - public ListenableFuture apply(Session session) throws Exception { - return session.executeAsync(useQuery); - } - }); - ListenableFuture withErrorHandling = - GuavaCompatibility.INSTANCE.withFallback( - keyspaceSet, - new AsyncFunction() { - @Override - public ListenableFuture apply(Throwable t) throws Exception { - session.closeAsync(); - if (t instanceof SyntaxError) { - // Give a more explicit message, because it's probably caused by a bad keyspace - // name - SyntaxError e = (SyntaxError) t; - t = - new SyntaxError( - e.getEndPoint(), - String.format( - "Error executing \"%s\" (%s). Check that your keyspace name is valid", - useQuery, e.getMessage())); - } - throw Throwables.propagate(t); - } - }); - return GuavaCompatibility.INSTANCE.transform(withErrorHandling, Functions.constant(session)); - } - } - - /** - * The name of this cluster object. - * - *

Note that this is not the Cassandra cluster name, but rather a name assigned to this Cluster - * object. Currently, that name is only used for one purpose: to distinguish exposed JMX metrics - * when multiple Cluster instances live in the same JVM (which should be rare in the first place). - * That name can be set at Cluster building time (through {@link Builder#withClusterName} for - * instance) but will default to a name like {@code cluster1} where each Cluster instance in the - * same JVM will have a different number. - * - * @return the name for this cluster instance. - */ - public String getClusterName() { - return manager.clusterName; - } - - /** - * Returns read-only metadata on the connected cluster. - * - *

This includes the known nodes with their status as seen by the driver, as well as the schema - * definitions. Since this return metadata on the connected cluster, this method may trigger the - * creation of a connection if none has been established yet (neither {@code init()} nor {@code - * connect()} has been called yet). - * - * @return the cluster metadata. - * @throws NoHostAvailableException if the Cluster has not been initialized yet and no host - * amongst the contact points can be reached. - * @throws AuthenticationException if an authentication error occurs while contacting the initial - * contact points. - * @throws IllegalStateException if the Cluster was closed prior to calling this method. This can - * occur either directly (through {@link #close()} or {@link #closeAsync()}), or as a result - * of an error while initializing the Cluster. - */ - public Metadata getMetadata() { - manager.init(); - return manager.metadata; - } - - /** - * The cluster configuration. - * - * @return the cluster configuration. - */ - public Configuration getConfiguration() { - return manager.configuration; - } - - /** - * The cluster metrics. - * - * @return the cluster metrics, or {@code null} if this cluster has not yet been {@link #init() - * initialized}, or if metrics collection has been disabled (that is if {@link - * Configuration#getMetricsOptions} returns {@code null}). - */ - public Metrics getMetrics() { - checkNotClosed(manager); - return manager.metrics; - } - - /** - * Registers the provided listener to be notified on hosts up/down/added/removed events. - * - *

Registering the same listener multiple times is a no-op. - * - *

This method should be used to register additional listeners on an already-initialized - * cluster. To add listeners to a cluster object prior to its initialization, use {@link - * Builder#withInitialListeners(Collection)}. Calling this method on a non-initialized cluster - * will result in the listener being {@link - * com.datastax.driver.core.Host.StateListener#onRegister(Cluster) notified} twice of cluster - * registration: once inside this method, and once at cluster initialization. - * - * @param listener the new {@link Host.StateListener} to register. - * @return this {@code Cluster} object; - */ - public Cluster register(Host.StateListener listener) { - checkNotClosed(manager); - boolean added = manager.listeners.add(listener); - if (added) listener.onRegister(this); - return this; - } - - /** - * Unregisters the provided listener from being notified on hosts events. - * - *

This method is a no-op if {@code listener} hasn't previously been registered against this - * Cluster. - * - * @param listener the {@link Host.StateListener} to unregister. - * @return this {@code Cluster} object; - */ - public Cluster unregister(Host.StateListener listener) { - checkNotClosed(manager); - boolean removed = manager.listeners.remove(listener); - if (removed) listener.onUnregister(this); - return this; - } - - /** - * Registers the provided tracker to be updated with hosts read latencies. - * - *

Registering the same tracker multiple times is a no-op. - * - *

Beware that the registered tracker's {@link LatencyTracker#update(Host, Statement, - * Exception, long) update} method will be called very frequently (at the end of every query to a - * Cassandra host) and should thus not be costly. - * - *

The main use case for a {@link LatencyTracker} is to allow load balancing policies to - * implement latency awareness. For example, {@link LatencyAwarePolicy} registers it's own - * internal {@code LatencyTracker} (automatically, you don't have to call this method directly). - * - * @param tracker the new {@link LatencyTracker} to register. - * @return this {@code Cluster} object; - */ - public Cluster register(LatencyTracker tracker) { - checkNotClosed(manager); - boolean added = manager.latencyTrackers.add(tracker); - if (added) tracker.onRegister(this); - return this; - } - - /** - * Unregisters the provided latency tracking from being updated with host read latencies. - * - *

This method is a no-op if {@code tracker} hasn't previously been registered against this - * Cluster. - * - * @param tracker the {@link LatencyTracker} to unregister. - * @return this {@code Cluster} object; - */ - public Cluster unregister(LatencyTracker tracker) { - checkNotClosed(manager); - boolean removed = manager.latencyTrackers.remove(tracker); - if (removed) tracker.onUnregister(this); - return this; - } - - /** - * Registers the provided listener to be updated with schema change events. - * - *

Registering the same listener multiple times is a no-op. - * - * @param listener the new {@link SchemaChangeListener} to register. - * @return this {@code Cluster} object; - */ - public Cluster register(SchemaChangeListener listener) { - checkNotClosed(manager); - boolean added = manager.schemaChangeListeners.add(listener); - if (added) listener.onRegister(this); - return this; - } - - /** - * Unregisters the provided schema change listener from being updated with schema change events. - * - *

This method is a no-op if {@code listener} hasn't previously been registered against this - * Cluster. - * - * @param listener the {@link SchemaChangeListener} to unregister. - * @return this {@code Cluster} object; - */ - public Cluster unregister(SchemaChangeListener listener) { - checkNotClosed(manager); - boolean removed = manager.schemaChangeListeners.remove(listener); - if (removed) listener.onUnregister(this); - return this; - } - - /** - * Initiates a shutdown of this cluster instance. - * - *

This method is asynchronous and return a future on the completion of the shutdown process. - * As soon a the cluster is shutdown, no new request will be accepted, but already submitted - * queries are allowed to complete. This method closes all connections from all sessions and - * reclaims all resources used by this Cluster instance. - * - *

If for some reason you wish to expedite this process, the {@link CloseFuture#force} can be - * called on the result future. - * - *

This method has no particular effect if the cluster was already closed (in which case the - * returned future will return immediately). - * - * @return a future on the completion of the shutdown process. - */ - public CloseFuture closeAsync() { - return manager.close(); - } - - /** - * Initiates a shutdown of this cluster instance and blocks until that shutdown completes. - * - *

This method is a shortcut for {@code closeAsync().get()}. - */ - @Override - public void close() { - try { - closeAsync().get(); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - /** - * Whether this Cluster instance has been closed. - * - *

Note that this method returns true as soon as one of the close methods ({@link #closeAsync} - * or {@link #close}) has been called, it does not guarantee that the closing is done. If you want - * to guarantee that the closing is done, you can call {@code close()} and wait until it returns - * (or call the get method on {@code closeAsync()} with a very short timeout and check this - * doesn't timeout). - * - * @return {@code true} if this Cluster instance has been closed, {@code false} otherwise. - */ - public boolean isClosed() { - return manager.closeFuture.get() != null; - } - - private static void checkNotClosed(Manager manager) { - if (manager.errorDuringInit()) { - throw new IllegalStateException( - "Can't use this cluster instance because it encountered an error in its initialization", - manager.getInitException()); - } else if (manager.isClosed()) { - throw new IllegalStateException( - "Can't use this cluster instance because it was previously closed"); - } - } - - /** - * Initializer for {@link Cluster} instances. - * - *

If you want to create a new {@code Cluster} instance programmatically, then it is advised to - * use {@link Cluster.Builder} which can be obtained from the {@link Cluster#builder} method. - * - *

But it is also possible to implement a custom {@code Initializer} that retrieves - * initialization from a web-service or from a configuration file. - */ - public interface Initializer { - - /** - * An optional name for the created cluster. - * - *

Such name is optional (a default name will be created otherwise) and is currently only use - * for JMX reporting of metrics. See {@link Cluster#getClusterName} for more information. - * - * @return the name for the created cluster or {@code null} to use an automatically generated - * name. - */ - public String getClusterName(); - - /** - * Returns the initial Cassandra hosts to connect to. - * - * @return the initial Cassandra contact points. See {@link Builder#addContactPoint} for more - * details on contact points. - */ - public List getContactPoints(); - - /** - * The configuration to use for the new cluster. - * - *

Note that some configuration can be modified after the cluster initialization but some - * others cannot. In particular, the ones that cannot be changed afterwards includes: - * - *

    - *
  • the port use to connect to Cassandra nodes (see {@link ProtocolOptions}). - *
  • the policies used (see {@link Policies}). - *
  • the authentication info provided (see {@link Configuration}). - *
  • whether metrics are enabled (see {@link Configuration}). - *
- * - * @return the configuration to use for the new cluster. - */ - public Configuration getConfiguration(); - - /** - * Optional listeners to register against the newly created cluster. - * - *

Note that contrary to listeners registered post Cluster creation, the listeners returned - * by this method will see {@link Host.StateListener#onAdd} events for the initial contact - * points. - * - * @return a possibly empty collection of {@code Host.StateListener} to register against the - * newly created cluster. - */ - public Collection getInitialListeners(); - } - - /** Helper class to build {@link Cluster} instances. */ - public static class Builder implements Initializer { - - private String clusterName; - private final List rawHostAndPortContactPoints = - new ArrayList(); - private final List rawHostContactPoints = new ArrayList(); - private final List contactPoints = new ArrayList(); - private int port = ProtocolOptions.DEFAULT_PORT; - private int maxSchemaAgreementWaitSeconds = - ProtocolOptions.DEFAULT_MAX_SCHEMA_AGREEMENT_WAIT_SECONDS; - private ProtocolVersion protocolVersion; - private AuthProvider authProvider = AuthProvider.NONE; - - private final Policies.Builder policiesBuilder = Policies.builder(); - private final Configuration.Builder configurationBuilder = Configuration.builder(); - - private ProtocolOptions.Compression compression = ProtocolOptions.Compression.NONE; - private SSLOptions sslOptions = null; - private boolean metricsEnabled = true; - private boolean jmxEnabled = true; - private boolean allowBetaProtocolVersion = false; - private boolean noCompact = false; - private boolean isCloud = false; - - private Collection listeners; - - @Override - public String getClusterName() { - return clusterName; - } - - @Override - public List getContactPoints() { - // Use a set to remove duplicate endpoints - Set allContactPoints = new LinkedHashSet(contactPoints); - // If contact points were provided as InetAddress/InetSocketAddress, assume the default - // endpoint factory is used. - for (InetAddress address : rawHostContactPoints) { - allContactPoints.add(new TranslatedAddressEndPoint(new InetSocketAddress(address, port))); - } - for (InetSocketAddress socketAddress : rawHostAndPortContactPoints) { - allContactPoints.add(new TranslatedAddressEndPoint(socketAddress)); - } - return new ArrayList(allContactPoints); - } - - /** - * An optional name for the create cluster. - * - *

Note: this is not related to the Cassandra cluster name (though you are free to provide - * the same name). See {@link Cluster#getClusterName} for details. - * - *

If you use this method and create more than one Cluster instance in the same JVM (which - * should be avoided unless you need to connect to multiple Cassandra clusters), you should make - * sure each Cluster instance get a unique name or you may have a problem with JMX reporting. - * - * @param name the cluster name to use for the created Cluster instance. - * @return this Builder. - */ - public Builder withClusterName(String name) { - this.clusterName = name; - return this; - } - - /** - * The port to use to connect to the Cassandra host. - * - *

If not set through this method, the default port (9042) will be used instead. - * - * @param port the port to set. - * @return this Builder. - */ - public Builder withPort(int port) { - this.port = port; - return this; - } - - /** - * Create cluster connection using latest development protocol version, which is currently in - * beta. Calling this method will result into setting USE_BETA flag in all outgoing messages, - * which allows server to negotiate the supported protocol version even if it is currently in - * beta. - * - *

This feature is only available starting with version {@link ProtocolVersion#V5 V5}. - * - *

Use with caution, refer to the server and protocol documentation for the details on latest - * protocol version. - * - * @return this Builder. - */ - public Builder allowBetaProtocolVersion() { - if (protocolVersion != null) - throw new IllegalArgumentException( - "Can't use beta flag with initial protocol version of " + protocolVersion); - - this.allowBetaProtocolVersion = true; - this.protocolVersion = ProtocolVersion.NEWEST_BETA; - return this; - } - - /** - * Sets the maximum time to wait for schema agreement before returning from a DDL query. - * - *

If not set through this method, the default value (10 seconds) will be used. - * - * @param maxSchemaAgreementWaitSeconds the new value to set. - * @return this Builder. - * @throws IllegalStateException if the provided value is zero or less. - */ - public Builder withMaxSchemaAgreementWaitSeconds(int maxSchemaAgreementWaitSeconds) { - if (maxSchemaAgreementWaitSeconds <= 0) - throw new IllegalArgumentException("Max schema agreement wait must be greater than zero"); - - this.maxSchemaAgreementWaitSeconds = maxSchemaAgreementWaitSeconds; - return this; - } - - /** - * The native protocol version to use. - * - *

The driver supports versions 1 to 5 of the native protocol. Higher versions of the - * protocol have more features and should be preferred, but this also depends on the Cassandra - * version: - * - *

- * - * - * - * - * - * - * - * - * - *
Native protocol version to Cassandra version correspondence
Protocol versionMinimum Cassandra version
11.2
22.0
32.1
42.2
53.10
- * - *

By default, the driver will "auto-detect" which protocol version it can use when - * connecting to the first node. More precisely, it will try first with {@link - * ProtocolVersion#NEWEST_SUPPORTED}, and if not supported fallback to the highest version - * supported by the first node it connects to. Please note that once the version is - * "auto-detected", it won't change: if the first node the driver connects to is a Cassandra 1.2 - * node and auto-detection is used (the default), then the native protocol version 1 will be use - * for the lifetime of the Cluster instance. - * - *

By using {@link Builder#allowBetaProtocolVersion()}, it is possible to force driver to - * connect to Cassandra node that supports the latest protocol beta version. Leaving this flag - * out will let client to connect with latest released version. - * - *

This method allows to force the use of a particular protocol version. Forcing version 1 is - * always fine since all Cassandra version (at least all those supporting the native protocol in - * the first place) so far support it. However, please note that a number of features of the - * driver won't be available if that version of the protocol is in use, including result set - * paging, {@link BatchStatement}, executing a non-prepared query with binary values ({@link - * Session#execute(String, Object...)}), ... (those methods will throw an - * UnsupportedFeatureException). Using the protocol version 1 should thus only be considered - * when using Cassandra 1.2, until nodes have been upgraded to Cassandra 2.0. - * - *

If version 2 of the protocol is used, then Cassandra 1.2 nodes will be ignored (the driver - * won't connect to them). - * - *

The default behavior (auto-detection) is fine in almost all case, but you may want to - * force a particular version if you have a Cassandra cluster with mixed 1.2/2.0 nodes (i.e. - * during a Cassandra upgrade). - * - * @param version the native protocol version to use. {@code null} is also supported to trigger - * auto-detection (see above) but this is the default (so you don't have to call this method - * for that behavior). - * @return this Builder. - */ - public Builder withProtocolVersion(ProtocolVersion version) { - if (allowBetaProtocolVersion) - throw new IllegalStateException( - "Can not set the version explicitly if `allowBetaProtocolVersion` was used."); - if (version.compareTo(ProtocolVersion.NEWEST_SUPPORTED) > 0) - throw new IllegalArgumentException( - "Can not use " - + version - + " protocol version. " - + "Newest supported protocol version is: " - + ProtocolVersion.NEWEST_SUPPORTED - + ". " - + "For beta versions, use `allowBetaProtocolVersion` instead"); - this.protocolVersion = version; - return this; - } - - /** - * Adds a contact point - or many if the given address resolves to multiple InetAddress - * s (A records). - * - *

Contact points are addresses of Cassandra nodes that the driver uses to discover the - * cluster topology. Only one contact point is required (the driver will retrieve the address of - * the other nodes automatically), but it is usually a good idea to provide more than one - * contact point, because if that single contact point is unavailable, the driver cannot - * initialize itself correctly. - * - *

Note that by default (that is, unless you use the {@link #withLoadBalancingPolicy}) method - * of this builder), the first successfully contacted host will be used to define the local - * data-center for the client. If follows that if you are running Cassandra in a multiple - * data-center setting, it is a good idea to only provide contact points that are in the same - * datacenter than the client, or to provide manually the load balancing policy that suits your - * need. - * - *

If the host name points to a DNS record with multiple a-records, all InetAddresses - * returned will be used. Make sure that all resulting InetAddresss returned point - * to the same cluster and datacenter. - * - * @param address the address of the node(s) to connect to. - * @return this Builder. - * @throws IllegalArgumentException if the given {@code address} could not be resolved. - * @throws SecurityException if a security manager is present and permission to resolve the host - * name is denied. - */ - public Builder addContactPoint(String address) { - // We explicitly check for nulls because InetAdress.getByName() will happily - // accept it and use localhost (while a null here almost likely mean a user error, - // not "connect to localhost") - failIfCloud(); - if (address == null) throw new NullPointerException(); - - try { - InetAddress[] allByName = InetAddress.getAllByName(address); - Collections.addAll(this.rawHostContactPoints, allByName); - return this; - } catch (UnknownHostException e) { - throw new IllegalArgumentException("Failed to add contact point: " + address, e); - } - } - - /** - * Adds a contact point using the given connection information. - * - *

You only need this method if you use a custom connection mechanism and have configured a - * custom {@link EndPointFactory}; otherwise, you can safely ignore it and use the higher level, - * host-and-port-based variants such as {@link #addContactPoint(String)}. - */ - public Builder addContactPoint(EndPoint contactPoint) { - failIfCloud(); - contactPoints.add(contactPoint); - return this; - } - - /** - * Adds contact points. - * - *

See {@link Builder#addContactPoint} for more details on contact points. - * - *

Note that all contact points must be resolvable; if any of them cannot be - * resolved, this method will fail. - * - * @param addresses addresses of the nodes to add as contact points. - * @return this Builder. - * @throws IllegalArgumentException if any of the given {@code addresses} could not be resolved. - * @throws SecurityException if a security manager is present and permission to resolve the host - * name is denied. - * @see Builder#addContactPoint - */ - public Builder addContactPoints(String... addresses) { - for (String address : addresses) addContactPoint(address); - return this; - } - - /** - * Adds contact points. - * - *

See {@link Builder#addContactPoint} for more details on contact points. - * - *

Note that all contact points must be resolvable; if any of them cannot be - * resolved, this method will fail. - * - * @param addresses addresses of the nodes to add as contact points. - * @return this Builder. - * @throws IllegalArgumentException if any of the given {@code addresses} could not be resolved. - * @throws SecurityException if a security manager is present and permission to resolve the host - * name is denied. - * @see Builder#addContactPoint - */ - public Builder addContactPoints(InetAddress... addresses) { - failIfCloud(); - Collections.addAll(this.rawHostContactPoints, addresses); - return this; - } - - /** - * Adds contact points. - * - *

See {@link Builder#addContactPoint} for more details on contact points. - * - * @param addresses addresses of the nodes to add as contact points. - * @return this Builder - * @see Builder#addContactPoint - */ - public Builder addContactPoints(Collection addresses) { - failIfCloud(); - this.rawHostContactPoints.addAll(addresses); - return this; - } - - /** - * Adds contact points. - * - *

See {@link Builder#addContactPoint} for more details on contact points. Contrarily to - * other {@code addContactPoints} methods, this method allows to provide a different port for - * each contact point. Since Cassandra nodes must always all listen on the same port, this is - * rarely what you want and most users should prefer other {@code addContactPoints} methods to - * this one. However, this can be useful if the Cassandra nodes are behind a router and are not - * accessed directly. Note that if you are in this situation (Cassandra nodes are behind a - * router, not directly accessible), you almost surely want to provide a specific {@link - * AddressTranslator} (through {@link #withAddressTranslator}) to translate actual Cassandra - * node addresses to the addresses the driver should use, otherwise the driver will not be able - * to auto-detect new nodes (and will generally not function optimally). - * - * @param addresses addresses of the nodes to add as contact points. - * @return this Builder - * @see Builder#addContactPoint - */ - public Builder addContactPointsWithPorts(InetSocketAddress... addresses) { - failIfCloud(); - Collections.addAll(this.rawHostAndPortContactPoints, addresses); - return this; - } - - /** - * Adds contact points. - * - *

See {@link Builder#addContactPoint} for more details on contact points. Contrarily to - * other {@code addContactPoints} methods, this method allows to provide a different port for - * each contact point. Since Cassandra nodes must always all listen on the same port, this is - * rarely what you want and most users should prefer other {@code addContactPoints} methods to - * this one. However, this can be useful if the Cassandra nodes are behind a router and are not - * accessed directly. Note that if you are in this situation (Cassandra nodes are behind a - * router, not directly accessible), you almost surely want to provide a specific {@link - * AddressTranslator} (through {@link #withAddressTranslator}) to translate actual Cassandra - * node addresses to the addresses the driver should use, otherwise the driver will not be able - * to auto-detect new nodes (and will generally not function optimally). - * - * @param addresses addresses of the nodes to add as contact points. - * @return this Builder - * @see Builder#addContactPoint - */ - public Builder addContactPointsWithPorts(Collection addresses) { - failIfCloud(); - this.rawHostAndPortContactPoints.addAll(addresses); - return this; - } - - /** - * Configures the load balancing policy to use for the new cluster. - * - *

If no load balancing policy is set through this method, {@link - * Policies#defaultLoadBalancingPolicy} will be used instead. - * - * @param policy the load balancing policy to use. - * @return this Builder. - */ - public Builder withLoadBalancingPolicy(LoadBalancingPolicy policy) { - policiesBuilder.withLoadBalancingPolicy(policy); - return this; - } - - /** - * Configures the reconnection policy to use for the new cluster. - * - *

If no reconnection policy is set through this method, {@link - * Policies#DEFAULT_RECONNECTION_POLICY} will be used instead. - * - * @param policy the reconnection policy to use. - * @return this Builder. - */ - public Builder withReconnectionPolicy(ReconnectionPolicy policy) { - policiesBuilder.withReconnectionPolicy(policy); - return this; - } - - /** - * Configures the retry policy to use for the new cluster. - * - *

If no retry policy is set through this method, {@link Policies#DEFAULT_RETRY_POLICY} will - * be used instead. - * - * @param policy the retry policy to use. - * @return this Builder. - */ - public Builder withRetryPolicy(RetryPolicy policy) { - policiesBuilder.withRetryPolicy(policy); - return this; - } - - /** - * Configures the address translator to use for the new cluster. - * - *

See {@link AddressTranslator} for more detail on address translation, but the default - * translator, {@link IdentityTranslator}, should be correct in most cases. If unsure, stick to - * the default. - * - * @param translator the translator to use. - * @return this Builder. - */ - public Builder withAddressTranslator(AddressTranslator translator) { - policiesBuilder.withAddressTranslator(translator); - return this; - } - - /** - * Configures the generator that will produce the client-side timestamp sent with each query. - * - *

This feature is only available with version {@link ProtocolVersion#V3 V3} or above of the - * native protocol. With earlier versions, timestamps are always generated server-side, and - * setting a generator through this method will have no effect. - * - *

If no generator is set through this method, the driver will default to client-side - * timestamps by using {@link AtomicMonotonicTimestampGenerator}. - * - * @param timestampGenerator the generator to use. - * @return this Builder. - */ - public Builder withTimestampGenerator(TimestampGenerator timestampGenerator) { - policiesBuilder.withTimestampGenerator(timestampGenerator); - return this; - } - - /** - * Configures the speculative execution policy to use for the new cluster. - * - *

If no policy is set through this method, {@link - * Policies#defaultSpeculativeExecutionPolicy()} will be used instead. - * - * @param policy the policy to use. - * @return this Builder. - */ - public Builder withSpeculativeExecutionPolicy(SpeculativeExecutionPolicy policy) { - policiesBuilder.withSpeculativeExecutionPolicy(policy); - return this; - } - - /** - * Configures the endpoint factory to use for the new cluster. - * - *

This is a low-level component for advanced scenarios where connecting to a node requires - * more than its socket address. If you're simply using host+port, the default factory is - * sufficient. - */ - public Builder withEndPointFactory(EndPointFactory endPointFactory) { - policiesBuilder.withEndPointFactory(endPointFactory); - return this; - } - - /** - * Configures the {@link CodecRegistry} instance to use for the new cluster. - * - *

If no codec registry is set through this method, {@link CodecRegistry#DEFAULT_INSTANCE} - * will be used instead. - * - *

Note that if two or more {@link Cluster} instances are configured to use the default codec - * registry, they are going to share the same instance. In this case, care should be taken when - * registering new codecs on it as any codec registered by one cluster would be immediately - * available to others sharing the same default instance. - * - * @param codecRegistry the codec registry to use. - * @return this Builder. - */ - public Builder withCodecRegistry(CodecRegistry codecRegistry) { - configurationBuilder.withCodecRegistry(codecRegistry); - return this; - } - - /** - * Uses the provided credentials when connecting to Cassandra hosts. - * - *

This should be used if the Cassandra cluster has been configured to use the {@code - * PasswordAuthenticator}. If the the default {@code AllowAllAuthenticator} is used instead, - * using this method has no effect. - * - * @param username the username to use to login to Cassandra hosts. - * @param password the password corresponding to {@code username}. - * @return this Builder. - */ - public Builder withCredentials(String username, String password) { - this.authProvider = new PlainTextAuthProvider(username, password); - return this; - } - - /** - * Use the specified AuthProvider when connecting to Cassandra hosts. - * - *

Use this method when a custom authentication scheme is in place. You shouldn't call both - * this method and {@code withCredentials} on the same {@code Builder} instance as one will - * supersede the other - * - * @param authProvider the {@link AuthProvider} to use to login to Cassandra hosts. - * @return this Builder - */ - public Builder withAuthProvider(AuthProvider authProvider) { - this.authProvider = authProvider; - return this; - } - - /** - * Sets the compression to use for the transport. - * - * @param compression the compression to set. - * @return this Builder. - * @see ProtocolOptions.Compression - */ - public Builder withCompression(ProtocolOptions.Compression compression) { - this.compression = compression; - return this; - } - - /** - * Disables metrics collection for the created cluster (metrics are enabled by default - * otherwise). - * - * @return this builder. - */ - public Builder withoutMetrics() { - this.metricsEnabled = false; - return this; - } - - /** - * Enables the use of SSL for the created {@code Cluster}. - * - *

Calling this method will use the JDK-based implementation with the default options (see - * {@link RemoteEndpointAwareJdkSSLOptions.Builder}). This is thus a shortcut for {@code - * withSSL(JdkSSLOptions.builder().build())}. - * - *

Note that if SSL is enabled, the driver will not connect to any Cassandra nodes that - * doesn't have SSL enabled and it is strongly advised to enable SSL on every Cassandra node if - * you plan on using SSL in the driver. - * - * @return this builder. - */ - public Builder withSSL() { - this.sslOptions = RemoteEndpointAwareJdkSSLOptions.builder().build(); - return this; - } - - /** - * Enable the use of SSL for the created {@code Cluster} using the provided options. - * - * @param sslOptions the SSL options to use. - * @return this builder. - */ - public Builder withSSL(SSLOptions sslOptions) { - this.sslOptions = sslOptions; - return this; - } - - /** - * Register the provided listeners in the newly created cluster. - * - *

Note: repeated calls to this method will override the previous ones. - * - * @param listeners the listeners to register. - * @return this builder. - */ - public Builder withInitialListeners(Collection listeners) { - this.listeners = listeners; - return this; - } - - /** - * Disables JMX reporting of the metrics. - * - *

JMX reporting is enabled by default (see {@link Metrics}) but can be disabled using this - * option. If metrics are disabled, this is a no-op. - * - * @return this builder. - */ - public Builder withoutJMXReporting() { - this.jmxEnabled = false; - return this; - } - - /** - * Sets the PoolingOptions to use for the newly created Cluster. - * - *

If no pooling options are set through this method, default pooling options will be used. - * - * @param options the pooling options to use. - * @return this builder. - */ - public Builder withPoolingOptions(PoolingOptions options) { - configurationBuilder.withPoolingOptions(options); - return this; - } - - /** - * Sets the SocketOptions to use for the newly created Cluster. - * - *

If no socket options are set through this method, default socket options will be used. - * - * @param options the socket options to use. - * @return this builder. - */ - public Builder withSocketOptions(SocketOptions options) { - configurationBuilder.withSocketOptions(options); - return this; - } - - /** - * Sets the QueryOptions to use for the newly created Cluster. - * - *

If no query options are set through this method, default query options will be used. - * - * @param options the query options to use. - * @return this builder. - */ - public Builder withQueryOptions(QueryOptions options) { - configurationBuilder.withQueryOptions(options); - return this; - } - - /** - * Sets the threading options to use for the newly created Cluster. - * - *

If no options are set through this method, a new instance of {@link ThreadingOptions} will - * be used. - * - * @param options the options. - * @return this builder. - */ - public Builder withThreadingOptions(ThreadingOptions options) { - configurationBuilder.withThreadingOptions(options); - return this; - } - - /** - * Set the {@link NettyOptions} to use for the newly created Cluster. - * - *

If no Netty options are set through this method, {@link NettyOptions#DEFAULT_INSTANCE} - * will be used as a default value, which means that no customization will be applied. - * - * @param nettyOptions the {@link NettyOptions} to use. - * @return this builder. - */ - public Builder withNettyOptions(NettyOptions nettyOptions) { - configurationBuilder.withNettyOptions(nettyOptions); - return this; - } - - /** - * Enables the NO_COMPACT startup option. - *

- * When this option is supplied, SELECT, UPDATE, DELETE and - * BATCH statements on COMPACT STORAGE tables function in "compatibility" mode which - * allows seeing these tables as if they were "regular" CQL tables. - *

- * This option only effects interactions with tables using COMPACT STORAGE and is only supported by - * C* 4.0+ and DSE 6.0+. - * - * @return this builder. - * @see CASSANDRA-10857 - */ - public Builder withNoCompact() { - this.noCompact = true; - return this; - } - - /** - * Configures this Builder for Cloud deployments by retrieving connection information from the - * provided {@link String}. - * - *

To connect to a Cloud database, you must first download the secure database bundle from - * the DataStax Constellation console that contains the connection information, then instruct - * the driver to read its contents using either this method or one if its variants. - * - *

For more information, please refer to the DataStax Constellation documentation. - * - *

Note that the provided stream will be consumed and closed when this method will - * return; attempting to reuse it afterwards will result in an error being thrown. - * - * @param cloudConfigFile File that contains secure connect bundle zip file. - * @see #withCloudSecureConnectBundle(URL) - * @see #withCloudSecureConnectBundle(InputStream) - */ - public Builder withCloudSecureConnectBundle(File cloudConfigFile) { - try { - return withCloudSecureConnectBundle(cloudConfigFile.toURI().toURL()); - } catch (MalformedURLException e) { - throw new IllegalArgumentException( - "The cloudConfigFile URL " + cloudConfigFile + " is in the wrong format.", e); - } - } - - /** - * Configures this Builder for Cloud deployments by retrieving connection information from the - * provided {@link URL}. - * - *

To connect to a Cloud database, you must first download the secure database bundle from - * the DataStax Constellation console that contains the connection information, then instruct - * the driver to read its contents using either this method or one if its variants. - * - *

For more information, please refer to the DataStax Constellation documentation. - * - *

Note that the provided stream will be consumed and closed when this method will - * return; attempting to reuse it afterwards will result in an error being thrown. - * - * @param cloudConfigUrl URL to the secure connect bundle zip file. - * @see #withCloudSecureConnectBundle(File) - * @see #withCloudSecureConnectBundle(InputStream) - */ - public Builder withCloudSecureConnectBundle(URL cloudConfigUrl) { - CloudConfig cloudConfig; - try { - cloudConfig = new CloudConfigFactory().createCloudConfig(cloudConfigUrl.openStream()); - } catch (GeneralSecurityException e) { - throw new IllegalStateException( - "Cannot construct cloud config from the cloudConfigUrl: " + cloudConfigUrl, e); - } catch (IOException e) { - throw new IllegalStateException( - "Cannot construct cloud config from the cloudConfigUrl: " + cloudConfigUrl, e); - } - - return addCloudConfigToBuilder(cloudConfig); - } - - /** - * Configures this Builder for Cloud deployments by retrieving connection information from the - * provided {@link InputStream}. - * - *

To connect to a Cloud database, you must first download the secure database bundle from - * the DataStax Constellation console that contains the connection information, then instruct - * the driver to read its contents using either this method or one if its variants. - * - *

For more information, please refer to the DataStax Constellation documentation. - * - *

Note that the provided stream will be consumed and closed when this method will - * return; attempting to reuse it afterwards will result in an error being thrown. - * - * @param cloudConfigInputStream A stream containing the secure connect bundle zip file. - * @see #withCloudSecureConnectBundle(File) - * @see #withCloudSecureConnectBundle(URL) - */ - public Builder withCloudSecureConnectBundle(InputStream cloudConfigInputStream) { - CloudConfig cloudConfig; - try { - cloudConfig = new CloudConfigFactory().createCloudConfig(cloudConfigInputStream); - } catch (GeneralSecurityException e) { - throw new IllegalStateException("Cannot construct cloud config from the InputStream.", e); - } catch (IOException e) { - throw new IllegalStateException("Cannot construct cloud config from the InputStream.", e); - } - - return addCloudConfigToBuilder(cloudConfig); - } - - private Builder addCloudConfigToBuilder(CloudConfig cloudConfig) { - Builder builder = - withEndPointFactory(new SniEndPointFactory(cloudConfig.getProxyAddress())) - .withSSL(cloudConfig.getSslOptions()); - - if (cloudConfig.getAuthProvider() != null) { - builder = builder.withAuthProvider(cloudConfig.getAuthProvider()); - } - if (builder.rawHostContactPoints.size() > 0 - || builder.rawHostAndPortContactPoints.size() > 0 - || builder.contactPoints.size() > 0) { - throw new IllegalStateException( - "Can't use withCloudSecureConnectBundle if you've already called addContactPoint(s)"); - } - for (EndPoint endPoint : cloudConfig.getEndPoints()) { - builder.addContactPoint(endPoint); - } - isCloud = true; - return builder; - } - - private void failIfCloud() { - if (isCloud) { - throw new IllegalStateException( - "Can't use addContactPoint(s) if you've already called withCloudSecureConnectBundle"); - } - } - - /** - * The configuration that will be used for the new cluster. - * - *

You should not modify this object directly because changes made to the returned - * object may not be used by the cluster build. Instead, you should use the other methods of - * this {@code Builder}. - * - * @return the configuration to use for the new cluster. - */ - @Override - public Configuration getConfiguration() { - ProtocolOptions protocolOptions = - new ProtocolOptions( - port, - protocolVersion, - maxSchemaAgreementWaitSeconds, - sslOptions, - authProvider, - noCompact) - .setCompression(compression); - - MetricsOptions metricsOptions = new MetricsOptions(metricsEnabled, jmxEnabled); - - return configurationBuilder - .withProtocolOptions(protocolOptions) - .withMetricsOptions(metricsOptions) - .withPolicies(policiesBuilder.build()) - .build(); - } - - @Override - public Collection getInitialListeners() { - return listeners == null ? Collections.emptySet() : listeners; - } - - /** - * Builds the cluster with the configured set of initial contact points and policies. - * - *

This is a convenience method for {@code Cluster.buildFrom(this)}. - * - * @return the newly built Cluster instance. - */ - public Cluster build() { - return Cluster.buildFrom(this); - } - } - - static long timeSince(long startNanos, TimeUnit destUnit) { - return destUnit.convert(System.nanoTime() - startNanos, TimeUnit.NANOSECONDS); - } - - private static String generateClusterName() { - return "cluster" + CLUSTER_ID.incrementAndGet(); - } - - /** - * The sessions and hosts managed by this a Cluster instance. - * - *

Note: the reason we create a Manager object separate from Cluster is that Manager is not - * publicly visible. For instance, we wouldn't want user to be able to call the {@link #onUp} and - * {@link #onDown} methods. - */ - class Manager implements Connection.DefaultResponseHandler { - - final String clusterName; - private volatile boolean isInit; - private volatile boolean isFullyInit; - private Exception initException; - // Initial contacts point - final List contactPoints; - final Set sessions = new CopyOnWriteArraySet(); - - Metadata metadata; - final Configuration configuration; - Metrics metrics; - - Connection.Factory connectionFactory; - ControlConnection controlConnection; - - final ConvictionPolicy.Factory convictionPolicyFactory = - new ConvictionPolicy.DefaultConvictionPolicy.Factory(); - - ListeningExecutorService executor; - ListeningExecutorService blockingExecutor; - ScheduledExecutorService reconnectionExecutor; - ScheduledExecutorService scheduledTasksExecutor; - - BlockingQueue executorQueue; - BlockingQueue blockingExecutorQueue; - BlockingQueue reconnectionExecutorQueue; - BlockingQueue scheduledTasksExecutorQueue; - - ConnectionReaper reaper; - - final AtomicReference closeFuture = new AtomicReference(); - - // All the queries that have been prepared (we keep them so we can re-prepared them when a node - // fail or a - // new one join the cluster). - // Note: we could move this down to the session level, but since prepared statement are global - // to a node, - // this would yield a slightly less clear behavior. - ConcurrentMap preparedQueries; - - final Set listeners; - final Set latencyTrackers = new CopyOnWriteArraySet(); - final Set schemaChangeListeners = - new CopyOnWriteArraySet(); - - EventDebouncer nodeListRefreshRequestDebouncer; - EventDebouncer nodeRefreshRequestDebouncer; - EventDebouncer schemaRefreshRequestDebouncer; - - private Manager( - String clusterName, - List contactPoints, - Configuration configuration, - Collection listeners) { - this.clusterName = clusterName == null ? generateClusterName() : clusterName; - this.configuration = configuration; - this.contactPoints = contactPoints; - this.listeners = new CopyOnWriteArraySet(listeners); - } - - // Initialization is not too performance intensive and in practice there shouldn't be contention - // on it so synchronized is good enough. - synchronized void init() { - checkNotClosed(this); - if (isInit) { - return; - } - isInit = true; - try { - logger.debug("Starting new cluster with contact points " + contactPoints); - - this.configuration.register(this); - - ThreadingOptions threadingOptions = this.configuration.getThreadingOptions(); - - // executor - ExecutorService tmpExecutor = threadingOptions.createExecutor(clusterName); - this.executorQueue = - (tmpExecutor instanceof ThreadPoolExecutor) - ? ((ThreadPoolExecutor) tmpExecutor).getQueue() - : null; - this.executor = MoreExecutors.listeningDecorator(tmpExecutor); - - // blocking executor - ExecutorService tmpBlockingExecutor = threadingOptions.createBlockingExecutor(clusterName); - this.blockingExecutorQueue = - (tmpBlockingExecutor instanceof ThreadPoolExecutor) - ? ((ThreadPoolExecutor) tmpBlockingExecutor).getQueue() - : null; - this.blockingExecutor = MoreExecutors.listeningDecorator(tmpBlockingExecutor); - - // reconnection executor - this.reconnectionExecutor = threadingOptions.createReconnectionExecutor(clusterName); - this.reconnectionExecutorQueue = - (reconnectionExecutor instanceof ThreadPoolExecutor) - ? ((ThreadPoolExecutor) reconnectionExecutor).getQueue() - : null; - - // scheduled tasks executor - this.scheduledTasksExecutor = threadingOptions.createScheduledTasksExecutor(clusterName); - this.scheduledTasksExecutorQueue = - (scheduledTasksExecutor instanceof ThreadPoolExecutor) - ? ((ThreadPoolExecutor) scheduledTasksExecutor).getQueue() - : null; - - this.reaper = new ConnectionReaper(threadingOptions.createReaperExecutor(clusterName)); - this.metadata = new Metadata(this); - this.connectionFactory = new Connection.Factory(this, configuration); - this.controlConnection = new ControlConnection(this); - this.metrics = configuration.getMetricsOptions().isEnabled() ? new Metrics(this) : null; - this.preparedQueries = new MapMaker().weakValues().makeMap(); - - // create debouncers - at this stage, they are not running yet - final QueryOptions queryOptions = configuration.getQueryOptions(); - this.nodeListRefreshRequestDebouncer = - new EventDebouncer( - "Node list refresh", - scheduledTasksExecutor, - new NodeListRefreshRequestDeliveryCallback()) { - - @Override - int maxPendingEvents() { - return configuration.getQueryOptions().getMaxPendingRefreshNodeListRequests(); - } - - @Override - long delayMs() { - return configuration.getQueryOptions().getRefreshNodeListIntervalMillis(); - } - }; - this.nodeRefreshRequestDebouncer = - new EventDebouncer( - "Node refresh", scheduledTasksExecutor, new NodeRefreshRequestDeliveryCallback()) { - - @Override - int maxPendingEvents() { - return configuration.getQueryOptions().getMaxPendingRefreshNodeRequests(); - } - - @Override - long delayMs() { - return configuration.getQueryOptions().getRefreshNodeIntervalMillis(); - } - }; - this.schemaRefreshRequestDebouncer = - new EventDebouncer( - "Schema refresh", - scheduledTasksExecutor, - new SchemaRefreshRequestDeliveryCallback()) { - - @Override - int maxPendingEvents() { - return configuration.getQueryOptions().getMaxPendingRefreshSchemaRequests(); - } - - @Override - long delayMs() { - return configuration.getQueryOptions().getRefreshSchemaIntervalMillis(); - } - }; - - this.scheduledTasksExecutor.scheduleWithFixedDelay( - new CleanupIdleConnectionsTask(), 10, 10, TimeUnit.SECONDS); - - for (EndPoint contactPoint : contactPoints) { - metadata.addContactPoint(contactPoint); - } - // Initialize the control connection: - negotiateProtocolVersionAndConnect(); - if (controlConnection.isCloud() && !configuration.getQueryOptions().isConsistencySet()) { - configuration.getQueryOptions().setConsistencyLevel(ConsistencyLevel.LOCAL_QUORUM); - } - // The control connection: - // - marked contact points down if they couldn't be reached - // - triggered an initial full refresh of metadata.allHosts. If any contact points weren't - // valid, they won't appear in it. - Set downContactPointHosts = Sets.newHashSet(); - Set removedContactPointHosts = Sets.newHashSet(); - for (Host contactPoint : metadata.getContactPoints()) { - if (!metadata.allHosts().contains(contactPoint)) { - removedContactPointHosts.add(contactPoint); - } else if (contactPoint.state == Host.State.DOWN) { - downContactPointHosts.add(contactPoint); - } - } - - // Now that the control connection is ready, we have all the information we need about the - // nodes (datacenter, rack...) to initialize the load balancing policy - Set lbpContactPoints = Sets.newHashSet(metadata.getContactPoints()); - lbpContactPoints.removeAll(removedContactPointHosts); - lbpContactPoints.removeAll(downContactPointHosts); - loadBalancingPolicy().init(Cluster.this, lbpContactPoints); - - speculativeExecutionPolicy().init(Cluster.this); - configuration.getPolicies().getRetryPolicy().init(Cluster.this); - reconnectionPolicy().init(Cluster.this); - configuration.getPolicies().getAddressTranslator().init(Cluster.this); - for (LatencyTracker tracker : latencyTrackers) tracker.onRegister(Cluster.this); - for (Host.StateListener listener : listeners) listener.onRegister(Cluster.this); - for (Host host : removedContactPointHosts) { - loadBalancingPolicy().onRemove(host); - for (Host.StateListener listener : listeners) listener.onRemove(host); - } - - for (Host host : downContactPointHosts) { - loadBalancingPolicy().onDown(host); - for (Host.StateListener listener : listeners) listener.onDown(host); - startPeriodicReconnectionAttempt(host, true); - } - - configuration.getPoolingOptions().setProtocolVersion(protocolVersion()); - - for (Host host : metadata.allHosts()) { - // If the host is down at this stage, it's a contact point that the control connection - // failed to reach. - // Reconnection attempts are already scheduled, and the LBP and listeners have been - // notified above. - if (host.state == Host.State.DOWN) continue; - - // Otherwise, we want to do the equivalent of onAdd(). But since we know for sure that no - // sessions or prepared - // statements exist at this point, we can skip some of the steps (plus this avoids - // scheduling concurrent pool - // creations if a session is created right after this method returns). - logger.info("New Cassandra host {} added", host); - - if (!host.supports(connectionFactory.protocolVersion)) { - logUnsupportedVersionProtocol(host, connectionFactory.protocolVersion); - continue; - } - - if (!lbpContactPoints.contains(host)) loadBalancingPolicy().onAdd(host); - - host.setUp(); - - for (Host.StateListener listener : listeners) listener.onAdd(host); - } - - // start debouncers - this.nodeListRefreshRequestDebouncer.start(); - this.schemaRefreshRequestDebouncer.start(); - this.nodeRefreshRequestDebouncer.start(); - - isFullyInit = true; - } catch (RuntimeException e) { - initException = e; - close(); - throw e; - } - } - - private void negotiateProtocolVersionAndConnect() { - boolean shouldNegotiate = (configuration.getProtocolOptions().initialProtocolVersion == null); - while (true) { - try { - controlConnection.connect(); - return; - } catch (UnsupportedProtocolVersionException e) { - if (!shouldNegotiate) { - throw e; - } - // Do not trust version of server's response, as C* behavior in case of protocol - // negotiation is not - // properly documented, and varies over time (specially after CASSANDRA-11464). Instead, - // always - // retry at attempted version - 1, if such a version exists; and otherwise, stop and fail. - ProtocolVersion attemptedVersion = e.getUnsupportedVersion(); - ProtocolVersion retryVersion = attemptedVersion.getLowerSupported(); - if (retryVersion == null) { - throw e; - } - logger.info( - "Cannot connect with protocol version {}, trying with {}", - attemptedVersion, - retryVersion); - connectionFactory.protocolVersion = retryVersion; - } - } - } - - ProtocolVersion protocolVersion() { - return connectionFactory.protocolVersion; - } - - Cluster getCluster() { - return Cluster.this; - } - - LoadBalancingPolicy loadBalancingPolicy() { - return configuration.getPolicies().getLoadBalancingPolicy(); - } - - SpeculativeExecutionPolicy speculativeExecutionPolicy() { - return configuration.getPolicies().getSpeculativeExecutionPolicy(); - } - - ReconnectionPolicy reconnectionPolicy() { - return configuration.getPolicies().getReconnectionPolicy(); - } - - InetSocketAddress translateAddress(InetSocketAddress address) { - InetSocketAddress translated = - configuration.getPolicies().getAddressTranslator().translate(address); - return translated == null ? address : translated; - } - - InetSocketAddress translateAddress(InetAddress address) { - InetSocketAddress sa = new InetSocketAddress(address, connectionFactory.getPort()); - return translateAddress(sa); - } - - private Session newSession() { - SessionManager session = new SessionManager(Cluster.this); - sessions.add(session); - return session; - } - - boolean removeSession(Session session) { - return sessions.remove(session); - } - - void reportQuery(Host host, Statement statement, Exception exception, long latencyNanos) { - for (LatencyTracker tracker : latencyTrackers) { - try { - tracker.update(host, statement, exception, latencyNanos); - } catch (Exception e) { - logger.error("Call to latency tracker failed", e); - } - } - } - - ControlConnection getControlConnection() { - return controlConnection; - } - - List getContactPoints() { - return contactPoints; - } - - boolean isClosed() { - return closeFuture.get() != null; - } - - boolean errorDuringInit() { - return (isInit && initException != null); - } - - Exception getInitException() { - return initException; - } - - private CloseFuture close() { - - CloseFuture future = closeFuture.get(); - if (future != null) return future; - - if (isInit) { - logger.debug("Shutting down"); - - // stop debouncers - if (nodeListRefreshRequestDebouncer != null) { - nodeListRefreshRequestDebouncer.stop(); - } - if (nodeRefreshRequestDebouncer != null) { - nodeRefreshRequestDebouncer.stop(); - } - if (schemaRefreshRequestDebouncer != null) { - schemaRefreshRequestDebouncer.stop(); - } - - // If we're shutting down, there is no point in waiting on scheduled reconnections, nor on - // notifications - // delivery or blocking tasks so we use shutdownNow - shutdownNow(reconnectionExecutor); - shutdownNow(scheduledTasksExecutor); - shutdownNow(blockingExecutor); - - // but for the worker executor, we want to let submitted tasks finish unless the shutdown is - // forced. - if (executor != null) { - executor.shutdown(); - } - - // We also close the metrics - if (metrics != null) metrics.shutdown(); - - loadBalancingPolicy().close(); - speculativeExecutionPolicy().close(); - configuration.getPolicies().getRetryPolicy().close(); - reconnectionPolicy().close(); - configuration.getPolicies().getAddressTranslator().close(); - for (LatencyTracker tracker : latencyTrackers) tracker.onUnregister(Cluster.this); - for (Host.StateListener listener : listeners) listener.onUnregister(Cluster.this); - for (SchemaChangeListener listener : schemaChangeListeners) - listener.onUnregister(Cluster.this); - - // Then we shutdown all connections - List futures = new ArrayList(sessions.size() + 1); - if (controlConnection != null) { - futures.add(controlConnection.closeAsync()); - } - for (Session session : sessions) futures.add(session.closeAsync()); - - future = new ClusterCloseFuture(futures); - // The rest will happen asynchronously, when all connections are successfully closed - } else { - future = CloseFuture.immediateFuture(); - } - - return closeFuture.compareAndSet(null, future) - ? future - : closeFuture.get(); // We raced, it's ok, return the future that was actually set - } - - private void shutdownNow(ExecutorService executor) { - if (executor != null) { - List pendingTasks = executor.shutdownNow(); - // If some tasks were submitted to this executor but not yet commenced, make sure the - // corresponding futures complete - for (Runnable pendingTask : pendingTasks) { - if (pendingTask instanceof FutureTask) ((FutureTask) pendingTask).cancel(false); - } - } - } - - void logUnsupportedVersionProtocol(Host host, ProtocolVersion version) { - logger.warn( - "Detected added or restarted Cassandra host {} but ignoring it since it does not support the version {} of the native " - + "protocol which is currently in use. If you want to force the use of a particular version of the native protocol, use " - + "Cluster.Builder#usingProtocolVersion() when creating the Cluster instance.", - host, - version); - } - - void logClusterNameMismatch(Host host, String expectedClusterName, String actualClusterName) { - logger.warn( - "Detected added or restarted Cassandra host {} but ignoring it since its cluster name '{}' does not match the one " - + "currently known ({})", - host, - actualClusterName, - expectedClusterName); - } - - public ListenableFuture triggerOnUp(final Host host) { - if (!isClosed()) { - return executor.submit( - new ExceptionCatchingRunnable() { - @Override - public void runMayThrow() throws InterruptedException, ExecutionException { - onUp(host, null); - } - }); - } else { - return MoreFutures.VOID_SUCCESS; - } - } - - // Use triggerOnUp unless you're sure you want to run this on the current thread. - private void onUp(final Host host, Connection reusedConnection) - throws InterruptedException, ExecutionException { - if (isClosed()) return; - - if (!host.supports(connectionFactory.protocolVersion)) { - logUnsupportedVersionProtocol(host, connectionFactory.protocolVersion); - return; - } - - try { - - boolean locked = - host.notificationsLock.tryLock(NOTIF_LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!locked) { - logger.warn( - "Could not acquire notifications lock within {} seconds, ignoring UP notification for {}", - NOTIF_LOCK_TIMEOUT_SECONDS, - host); - return; - } - try { - - // We don't want to use the public Host.isUp() as this would make us skip the rest for - // suspected hosts - if (host.state == Host.State.UP) return; - - Host.statesLogger.debug("[{}] marking host UP", host); - - // If there is a reconnection attempt scheduled for that node, cancel it - Future scheduledAttempt = host.reconnectionAttempt.getAndSet(null); - if (scheduledAttempt != null) { - logger.debug("Cancelling reconnection attempt since node is UP"); - scheduledAttempt.cancel(false); - } - - try { - if (getCluster().getConfiguration().getQueryOptions().isReprepareOnUp()) - reusedConnection = prepareAllQueries(host, reusedConnection); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - // Don't propagate because we don't want to prevent other listener to run - } catch (UnsupportedProtocolVersionException e) { - logUnsupportedVersionProtocol(host, e.getUnsupportedVersion()); - return; - } catch (ClusterNameMismatchException e) { - logClusterNameMismatch(host, e.expectedClusterName, e.actualClusterName); - return; - } - - // Session#onUp() expects the load balancing policy to have been updated first, so that - // Host distances are up to date. This mean the policy could return the node before the - // new pool have been created. This is harmless if there is no prior pool since - // RequestHandler - // will ignore the node, but we do want to make sure there is no prior pool so we don't - // query from a pool we will shutdown right away. - for (SessionManager s : sessions) s.removePool(host); - loadBalancingPolicy().onUp(host); - controlConnection.onUp(host); - - logger.trace("Adding/renewing host pools for newly UP host {}", host); - - List> futures = Lists.newArrayListWithCapacity(sessions.size()); - for (SessionManager s : sessions) futures.add(s.forceRenewPool(host, reusedConnection)); - - try { - // Only mark the node up once all session have re-added their pool (if the - // load-balancing - // policy says it should), so that Host.isUp() don't return true before we're - // reconnected - // to the node. - List poolCreationResults = Futures.allAsList(futures).get(); - - // If any of the creation failed, they will have signaled a connection failure - // which will trigger a reconnection to the node. So don't bother marking UP. - if (Iterables.any(poolCreationResults, Predicates.equalTo(false))) { - logger.debug("Connection pool cannot be created, not marking {} UP", host); - return; - } - - host.setUp(); - - for (Host.StateListener listener : listeners) listener.onUp(host); - - } catch (ExecutionException e) { - Throwable t = e.getCause(); - // That future is not really supposed to throw unexpected exceptions - if (!(t instanceof InterruptedException) && !(t instanceof CancellationException)) - logger.error( - "Unexpected error while marking node UP: while this shouldn't happen, this shouldn't be critical", - t); - } - - // Now, check if there isn't pools to create/remove following the addition. - // We do that now only so that it's not called before we've set the node up. - for (SessionManager s : sessions) s.updateCreatedPools().get(); - - } finally { - host.notificationsLock.unlock(); - } - - } finally { - if (reusedConnection != null && !reusedConnection.hasOwner()) reusedConnection.closeAsync(); - } - } - - public ListenableFuture triggerOnDown(final Host host, boolean startReconnection) { - return triggerOnDown(host, false, startReconnection); - } - - public ListenableFuture triggerOnDown( - final Host host, final boolean isHostAddition, final boolean startReconnection) { - if (!isClosed()) { - return executor.submit( - new ExceptionCatchingRunnable() { - @Override - public void runMayThrow() throws InterruptedException, ExecutionException { - onDown(host, isHostAddition, startReconnection); - } - }); - } else { - return MoreFutures.VOID_SUCCESS; - } - } - - // Use triggerOnDown unless you're sure you want to run this on the current thread. - private void onDown(final Host host, final boolean isHostAddition, boolean startReconnection) - throws InterruptedException, ExecutionException { - if (isClosed()) return; - - boolean locked = host.notificationsLock.tryLock(NOTIF_LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!locked) { - logger.warn( - "Could not acquire notifications lock within {} seconds, ignoring DOWN notification for {}", - NOTIF_LOCK_TIMEOUT_SECONDS, - host); - return; - } - try { - - // Note: we don't want to skip that method if !host.isUp() because we set isUp - // late in onUp, and so we can rely on isUp if there is an error during onUp. - // But if there is a reconnection attempt in progress already, then we know - // we've already gone through that method since the last successful onUp(), so - // we're good skipping it. - if (host.reconnectionAttempt.get() != null) { - logger.debug("Aborting onDown because a reconnection is running on DOWN host {}", host); - return; - } - - Host.statesLogger.debug("[{}] marking host DOWN", host); - - // Remember if we care about this node at all. We must call this before - // we've signalled the load balancing policy, since most policy will always - // IGNORE down nodes anyway. - HostDistance distance = loadBalancingPolicy().distance(host); - - boolean wasUp = host.isUp(); - host.setDown(); - - loadBalancingPolicy().onDown(host); - controlConnection.onDown(host); - for (SessionManager s : sessions) s.onDown(host); - - // Contrarily to other actions of that method, there is no reason to notify listeners - // unless the host was UP at the beginning of this function since even if a onUp fail - // mid-method, listeners won't have been notified of the UP. - if (wasUp) { - for (Host.StateListener listener : listeners) listener.onDown(host); - } - - // Don't start a reconnection if we ignore the node anyway (JAVA-314) - if (distance == HostDistance.IGNORED || !startReconnection) return; - - startPeriodicReconnectionAttempt(host, isHostAddition); - } finally { - host.notificationsLock.unlock(); - } - } - - void startPeriodicReconnectionAttempt(final Host host, final boolean isHostAddition) { - new AbstractReconnectionHandler( - host.toString(), - reconnectionExecutor, - reconnectionPolicy().newSchedule(), - host.reconnectionAttempt) { - - @Override - protected Connection tryReconnect() - throws ConnectionException, InterruptedException, UnsupportedProtocolVersionException, - ClusterNameMismatchException { - return connectionFactory.open(host); - } - - @Override - protected void onReconnection(Connection connection) { - // Make sure we have up-to-date infos on that host before adding it (so we typically - // catch that an upgraded node uses a new cassandra version). - if (controlConnection.refreshNodeInfo(host)) { - logger.debug("Successful reconnection to {}, setting host UP", host); - try { - if (isHostAddition) { - onAdd(host, connection); - submitNodeListRefresh(); - } else onUp(host, connection); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (Exception e) { - logger.error("Unexpected error while setting node up", e); - } - } else { - logger.debug("Not enough info for {}, ignoring host", host); - connection.closeAsync(); - } - } - - @Override - protected boolean onConnectionException(ConnectionException e, long nextDelayMs) { - if (logger.isDebugEnabled()) - logger.debug( - "Failed reconnection to {} ({}), scheduling retry in {} milliseconds", - host, - e.getMessage(), - nextDelayMs); - return true; - } - - @Override - protected boolean onUnknownException(Exception e, long nextDelayMs) { - logger.error( - String.format( - "Unknown error during reconnection to %s, scheduling retry in %d milliseconds", - host, nextDelayMs), - e); - return true; - } - - @Override - protected boolean onAuthenticationException(AuthenticationException e, long nextDelayMs) { - logger.error( - String.format( - "Authentication error during reconnection to %s, scheduling retry in %d milliseconds", - host, nextDelayMs), - e); - return true; - } - }.start(); - } - - void startSingleReconnectionAttempt(final Host host) { - if (isClosed() || host.isUp()) return; - - logger.debug("Scheduling one-time reconnection to {}", host); - - // Setting an initial delay of 0 to start immediately, and all the exception handlers return - // false to prevent further attempts - new AbstractReconnectionHandler( - host.toString(), - reconnectionExecutor, - reconnectionPolicy().newSchedule(), - host.reconnectionAttempt, - 0) { - - @Override - protected Connection tryReconnect() - throws ConnectionException, InterruptedException, UnsupportedProtocolVersionException, - ClusterNameMismatchException { - return connectionFactory.open(host); - } - - @Override - protected void onReconnection(Connection connection) { - // Make sure we have up-to-date infos on that host before adding it (so we typically - // catch that an upgraded node uses a new cassandra version). - if (controlConnection.refreshNodeInfo(host)) { - logger.debug("Successful reconnection to {}, setting host UP", host); - try { - onUp(host, connection); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (Exception e) { - logger.error("Unexpected error while setting node up", e); - } - } else { - logger.debug("Not enough info for {}, ignoring host", host); - connection.closeAsync(); - } - } - - @Override - protected boolean onConnectionException(ConnectionException e, long nextDelayMs) { - if (logger.isDebugEnabled()) - logger.debug("Failed one-time reconnection to {} ({})", host, e.getMessage()); - return false; - } - - @Override - protected boolean onUnknownException(Exception e, long nextDelayMs) { - logger.error(String.format("Unknown error during one-time reconnection to %s", host), e); - return false; - } - - @Override - protected boolean onAuthenticationException(AuthenticationException e, long nextDelayMs) { - logger.error( - String.format("Authentication error during one-time reconnection to %s", host), e); - return false; - } - }.start(); - } - - public ListenableFuture triggerOnAdd(final Host host) { - if (!isClosed()) { - return executor.submit( - new ExceptionCatchingRunnable() { - @Override - public void runMayThrow() throws InterruptedException, ExecutionException { - onAdd(host, null); - } - }); - } else { - return MoreFutures.VOID_SUCCESS; - } - } - - // Use triggerOnAdd unless you're sure you want to run this on the current thread. - private void onAdd(final Host host, Connection reusedConnection) - throws InterruptedException, ExecutionException { - if (isClosed()) return; - - if (!host.supports(connectionFactory.protocolVersion)) { - logUnsupportedVersionProtocol(host, connectionFactory.protocolVersion); - return; - } - - try { - - boolean locked = - host.notificationsLock.tryLock(NOTIF_LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!locked) { - logger.warn( - "Could not acquire notifications lock within {} seconds, ignoring ADD notification for {}", - NOTIF_LOCK_TIMEOUT_SECONDS, - host); - return; - } - try { - Host.statesLogger.debug("[{}] adding host", host); - - // Adds to the load balancing first and foremost, as doing so might change the decision - // it will make for distance() on that node (not likely but we leave that possibility). - // This does mean the policy may start returning that node for query plan, but as long - // as no pools have been created (below) this will be ignored by RequestHandler so it's - // fine. - loadBalancingPolicy().onAdd(host); - - // Next, if the host should be ignored, well, ignore it. - if (loadBalancingPolicy().distance(host) == HostDistance.IGNORED) { - // We still mark the node UP though as it should be (and notifiy the listeners). - // We'll mark it down if we have a notification anyway and we've documented that - // especially - // for IGNORED hosts, the isUp() method was a best effort guess - host.setUp(); - for (Host.StateListener listener : listeners) listener.onAdd(host); - return; - } - - try { - reusedConnection = prepareAllQueries(host, reusedConnection); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - // Don't propagate because we don't want to prevent other listener to run - } catch (UnsupportedProtocolVersionException e) { - logUnsupportedVersionProtocol(host, e.getUnsupportedVersion()); - return; - } catch (ClusterNameMismatchException e) { - logClusterNameMismatch(host, e.expectedClusterName, e.actualClusterName); - return; - } - - controlConnection.onAdd(host); - - List> futures = Lists.newArrayListWithCapacity(sessions.size()); - for (SessionManager s : sessions) futures.add(s.maybeAddPool(host, reusedConnection)); - - try { - // Only mark the node up once all session have added their pool (if the load-balancing - // policy says it should), so that Host.isUp() don't return true before we're - // reconnected - // to the node. - List poolCreationResults = Futures.allAsList(futures).get(); - - // If any of the creation failed, they will have signaled a connection failure - // which will trigger a reconnection to the node. So don't bother marking UP. - if (Iterables.any(poolCreationResults, Predicates.equalTo(false))) { - logger.debug("Connection pool cannot be created, not marking {} UP", host); - return; - } - - host.setUp(); - - for (Host.StateListener listener : listeners) listener.onAdd(host); - - } catch (ExecutionException e) { - Throwable t = e.getCause(); - // That future is not really supposed to throw unexpected exceptions - if (!(t instanceof InterruptedException) && !(t instanceof CancellationException)) - logger.error( - "Unexpected error while adding node: while this shouldn't happen, this shouldn't be critical", - t); - } - - // Now, check if there isn't pools to create/remove following the addition. - // We do that now only so that it's not called before we've set the node up. - for (SessionManager s : sessions) s.updateCreatedPools().get(); - - } finally { - host.notificationsLock.unlock(); - } - - } finally { - if (reusedConnection != null && !reusedConnection.hasOwner()) reusedConnection.closeAsync(); - } - } - - public ListenableFuture triggerOnRemove(final Host host) { - if (!isClosed()) { - return executor.submit( - new ExceptionCatchingRunnable() { - @Override - public void runMayThrow() throws InterruptedException, ExecutionException { - onRemove(host); - } - }); - } else { - return MoreFutures.VOID_SUCCESS; - } - } - - // Use triggerOnRemove unless you're sure you want to run this on the current thread. - private void onRemove(Host host) throws InterruptedException, ExecutionException { - if (isClosed()) return; - - boolean locked = host.notificationsLock.tryLock(NOTIF_LOCK_TIMEOUT_SECONDS, TimeUnit.SECONDS); - if (!locked) { - logger.warn( - "Could not acquire notifications lock within {} seconds, ignoring REMOVE notification for {}", - NOTIF_LOCK_TIMEOUT_SECONDS, - host); - return; - } - try { - - host.setDown(); - - Host.statesLogger.debug("[{}] removing host", host); - - loadBalancingPolicy().onRemove(host); - controlConnection.onRemove(host); - for (SessionManager s : sessions) s.onRemove(host); - - for (Host.StateListener listener : listeners) listener.onRemove(host); - } finally { - host.notificationsLock.unlock(); - } - } - - public void signalHostDown(Host host, boolean isHostAddition) { - // Don't mark the node down until we've fully initialized the controlConnection as this might - // mess up with - // the protocol detection - if (!isFullyInit || isClosed()) return; - - triggerOnDown(host, isHostAddition, true); - } - - public void removeHost(Host host, boolean isInitialConnection) { - if (host == null) return; - - if (metadata.remove(host)) { - if (isInitialConnection) { - logger.warn( - "You listed {} in your contact points, but it wasn't found in the control host's system.peers at startup", - host); - } else { - logger.info("Cassandra host {} removed", host); - triggerOnRemove(host); - } - } - } - - public void ensurePoolsSizing() { - if (protocolVersion().compareTo(ProtocolVersion.V3) >= 0) return; - - for (SessionManager session : sessions) { - for (HostConnectionPool pool : session.pools.values()) pool.ensureCoreConnections(); - } - } - - public PreparedStatement addPrepared(PreparedStatement stmt) { - PreparedStatement previous = - preparedQueries.putIfAbsent(stmt.getPreparedId().boundValuesMetadata.id, stmt); - if (previous != null) { - logger.warn( - "Re-preparing already prepared query is generally an anti-pattern and will likely affect performance. " - + "Consider preparing the statement only once. Query='{}'", - stmt.getQueryString()); - - // The one object in the cache will get GCed once it's not referenced by the client anymore - // since we use a weak reference. - // So we need to make sure that the instance we do return to the user is the one that is in - // the cache. - // However if the result metadata changed since the last PREPARE call, this also needs to be - // updated. - previous.getPreparedId().resultSetMetadata = stmt.getPreparedId().resultSetMetadata; - return previous; - } - return stmt; - } - - /** - * @param reusedConnection an existing connection (from a reconnection attempt) that we want to - * reuse to prepare the statements (might be null). - * @return a connection that the rest of the initialization process can use (it will be made - * part of a connection pool). Can be reusedConnection, or one that was open in the method. - */ - private Connection prepareAllQueries(Host host, Connection reusedConnection) - throws InterruptedException, UnsupportedProtocolVersionException, - ClusterNameMismatchException { - if (preparedQueries.isEmpty()) return reusedConnection; - - logger.debug( - "Preparing {} prepared queries on newly up node {}", preparedQueries.size(), host); - Connection connection = null; - try { - connection = (reusedConnection == null) ? connectionFactory.open(host) : reusedConnection; - - // Furthermore, along with each prepared query we keep the current keyspace at the time of - // preparation - // as we need to make it is the same when we re-prepare on new/restarted nodes. Most query - // will use the - // same keyspace so keeping it each time is slightly wasteful, but this doesn't really - // matter and is - // simpler. Besides, we do avoid in prepareAllQueries to not set the current keyspace more - // than needed. - - // We need to make sure we prepared every query with the right current keyspace, i.e. the - // one originally - // used for preparing it. However, since we are likely that all prepared query belong to - // only a handful - // of different keyspace (possibly only one), and to avoid setting the current keyspace more - // than needed, - // we first sort the query per keyspace. - SetMultimap perKeyspace = HashMultimap.create(); - for (PreparedStatement ps : preparedQueries.values()) { - // It's possible for a query to not have a current keyspace. But since null doesn't work - // well as - // map keys, we use the empty string instead (that is not a valid keyspace name). - String keyspace = ps.getQueryKeyspace() == null ? "" : ps.getQueryKeyspace(); - perKeyspace.put(keyspace, ps.getQueryString()); - } - - for (String keyspace : perKeyspace.keySet()) { - // Empty string mean no particular keyspace to set - if (!keyspace.isEmpty()) connection.setKeyspace(keyspace); - - List futures = - new ArrayList(preparedQueries.size()); - for (String query : perKeyspace.get(keyspace)) { - futures.add(connection.write(new Requests.Prepare(query))); - } - for (Connection.Future future : futures) { - try { - future.get(); - } catch (ExecutionException e) { - // This "might" happen if we drop a CF but haven't removed it's prepared queries - // (which we don't do - // currently). It's not a big deal however as if it's a more serious problem it'll - // show up later when - // the query is tried for execution. - logger.debug("Unexpected error while preparing queries on new/newly up host", e); - } - } - } - - return connection; - } catch (ConnectionException e) { - // Ignore, not a big deal - if (connection != null) connection.closeAsync(); - return null; - } catch (AuthenticationException e) { - // That's a bad news, but ignore at this point - if (connection != null) connection.closeAsync(); - return null; - } catch (BusyConnectionException e) { - // Ignore, not a big deal - // In theory the problem is transient so the connection could be reused later, but if the - // core pool size is 1 - // it's better to close this one so that we start with a fresh connection. - if (connection != null) connection.closeAsync(); - return null; - } - } - - ListenableFuture submitSchemaRefresh( - final SchemaElement targetType, - final String targetKeyspace, - final String targetName, - final List targetSignature) { - SchemaRefreshRequest request = - new SchemaRefreshRequest(targetType, targetKeyspace, targetName, targetSignature); - logger.trace("Submitting schema refresh: {}", request); - return schemaRefreshRequestDebouncer.eventReceived(request); - } - - ListenableFuture submitNodeListRefresh() { - logger.trace("Submitting node list and token map refresh"); - return nodeListRefreshRequestDebouncer.eventReceived(new NodeListRefreshRequest()); - } - - ListenableFuture submitNodeRefresh(InetSocketAddress address, HostEvent eventType) { - NodeRefreshRequest request = new NodeRefreshRequest(address, eventType); - logger.trace("Submitting node refresh: {}", request); - return nodeRefreshRequestDebouncer.eventReceived(request); - } - - // refresh the schema using the provided connection, and notice the future with the provided - // resultset once done - public void refreshSchemaAndSignal( - final Connection connection, - final DefaultResultSetFuture future, - final ResultSet rs, - final SchemaElement targetType, - final String targetKeyspace, - final String targetName, - final List targetSignature) { - if (logger.isDebugEnabled()) - logger.debug( - "Refreshing schema for {}{}", - targetType == null ? "everything" : targetKeyspace, - (targetType == KEYSPACE) ? "" : "." + targetName + " (" + targetType + ")"); - - maybeRefreshSchemaAndSignal( - connection, future, rs, targetType, targetKeyspace, targetName, targetSignature); - } - - public void waitForSchemaAgreementAndSignal( - final Connection connection, final DefaultResultSetFuture future, final ResultSet rs) { - maybeRefreshSchemaAndSignal(connection, future, rs, null, null, null, null); - } - - private void maybeRefreshSchemaAndSignal( - final Connection connection, - final DefaultResultSetFuture future, - final ResultSet rs, - final SchemaElement targetType, - final String targetKeyspace, - final String targetName, - final List targetSignature) { - final boolean refreshSchema = - (targetKeyspace != null); // if false, only wait for schema agreement - - executor.submit( - new Runnable() { - @Override - public void run() { - boolean schemaInAgreement = false; - try { - // Before refreshing the schema, wait for schema agreement so - // that querying a table just after having created it don't fail. - schemaInAgreement = - ControlConnection.waitForSchemaAgreement(connection, Cluster.Manager.this); - if (!schemaInAgreement) - logger.warn( - "No schema agreement from live replicas after {} s. The schema may not be up to date on some nodes.", - configuration.getProtocolOptions().getMaxSchemaAgreementWaitSeconds()); - - ListenableFuture schemaReady; - if (refreshSchema) { - schemaReady = - submitSchemaRefresh(targetType, targetKeyspace, targetName, targetSignature); - // JAVA-1120: skip debouncing delay and force immediate delivery - if (!schemaReady.isDone()) - schemaRefreshRequestDebouncer.scheduleImmediateDelivery(); - } else { - schemaReady = MoreFutures.VOID_SUCCESS; - } - final boolean finalSchemaInAgreement = schemaInAgreement; - schemaReady.addListener( - new Runnable() { - @Override - public void run() { - rs.getExecutionInfo().setSchemaInAgreement(finalSchemaInAgreement); - future.setResult(rs); - } - }, - GuavaCompatibility.INSTANCE.sameThreadExecutor()); - - } catch (Exception e) { - logger.warn("Error while waiting for schema agreement", e); - // This is not fatal, complete the future anyway - rs.getExecutionInfo().setSchemaInAgreement(schemaInAgreement); - future.setResult(rs); - } - } - }); - } - - // Called when some message has been received but has been initiated from the server (streamId < - // 0). - // This is called on an I/O thread, so all blocking operation must be done on an executor. - @Override - public void handle(Message.Response response) { - - if (!(response instanceof Responses.Event)) { - logger.error("Received an unexpected message from the server: {}", response); - return; - } - - final ProtocolEvent event = ((Responses.Event) response).event; - - logger.debug("Received event {}, scheduling delivery", response); - - switch (event.type) { - case TOPOLOGY_CHANGE: - ProtocolEvent.TopologyChange tpc = (ProtocolEvent.TopologyChange) event; - Host.statesLogger.debug("[{}] received event {}", tpc.node, tpc.change); - // Do NOT translate the address, it will be matched against Host.getBroadcastRpcAddress() - // to find the target host. - switch (tpc.change) { - case REMOVED_NODE: - submitNodeRefresh(tpc.node, HostEvent.REMOVED); - break; - default: - // If a node was added, we don't have enough information to create a new Host (we are - // missing it's ID) so trigger a full refresh - submitNodeListRefresh(); - break; - } - break; - case STATUS_CHANGE: - ProtocolEvent.StatusChange stc = (ProtocolEvent.StatusChange) event; - Host.statesLogger.debug("[{}] received event {}", stc.node, stc.status); - // Do NOT translate the address, it will be matched against Host.getBroadcastRpcAddress() - // to find the target host. - switch (stc.status) { - case UP: - submitNodeRefresh(stc.node, HostEvent.UP); - break; - case DOWN: - submitNodeRefresh(stc.node, HostEvent.DOWN); - break; - } - break; - case SCHEMA_CHANGE: - if (!configuration.getQueryOptions().isMetadataEnabled()) return; - - ProtocolEvent.SchemaChange scc = (ProtocolEvent.SchemaChange) event; - switch (scc.change) { - case CREATED: - case UPDATED: - submitSchemaRefresh( - scc.targetType, scc.targetKeyspace, scc.targetName, scc.targetSignature); - break; - case DROPPED: - if (scc.targetType == KEYSPACE) { - final KeyspaceMetadata removedKeyspace = - manager.metadata.removeKeyspace(scc.targetKeyspace); - if (removedKeyspace != null) { - executor.submit( - new Runnable() { - @Override - public void run() { - manager.metadata.triggerOnKeyspaceRemoved(removedKeyspace); - } - }); - } - } else { - KeyspaceMetadata keyspace = manager.metadata.keyspaces.get(scc.targetKeyspace); - if (keyspace == null) { - logger.warn( - "Received a DROPPED notification for {} {}.{}, but this keyspace is unknown in our metadata", - scc.targetType, - scc.targetKeyspace, - scc.targetName); - } else { - switch (scc.targetType) { - case TABLE: - // we can't tell whether it's a table or a view, - // but since two objects cannot have the same name, - // try removing both - final TableMetadata removedTable = keyspace.removeTable(scc.targetName); - if (removedTable != null) { - executor.submit( - new Runnable() { - @Override - public void run() { - manager.metadata.triggerOnTableRemoved(removedTable); - } - }); - } else { - final MaterializedViewMetadata removedView = - keyspace.removeMaterializedView(scc.targetName); - if (removedView != null) { - executor.submit( - new Runnable() { - @Override - public void run() { - manager.metadata.triggerOnMaterializedViewRemoved(removedView); - } - }); - } - } - break; - case TYPE: - final UserType removedType = keyspace.removeUserType(scc.targetName); - if (removedType != null) { - executor.submit( - new Runnable() { - @Override - public void run() { - manager.metadata.triggerOnUserTypeRemoved(removedType); - } - }); - } - break; - case FUNCTION: - final FunctionMetadata removedFunction = - keyspace.removeFunction( - Metadata.fullFunctionName(scc.targetName, scc.targetSignature)); - if (removedFunction != null) { - executor.submit( - new Runnable() { - @Override - public void run() { - manager.metadata.triggerOnFunctionRemoved(removedFunction); - } - }); - } - break; - case AGGREGATE: - final AggregateMetadata removedAggregate = - keyspace.removeAggregate( - Metadata.fullFunctionName(scc.targetName, scc.targetSignature)); - if (removedAggregate != null) { - executor.submit( - new Runnable() { - @Override - public void run() { - manager.metadata.triggerOnAggregateRemoved(removedAggregate); - } - }); - } - break; - } - } - } - break; - } - break; - } - } - - void refreshConnectedHosts() { - // Deal first with the control connection: if it's connected to a node that is not LOCAL, try - // reconnecting (thus letting the loadBalancingPolicy pick a better node) - Host ccHost = controlConnection.connectedHost(); - if (ccHost == null || loadBalancingPolicy().distance(ccHost) != HostDistance.LOCAL) - controlConnection.triggerReconnect(); - - try { - for (SessionManager s : sessions) - Uninterruptibles.getUninterruptibly(s.updateCreatedPools()); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } - - void refreshConnectedHost(Host host) { - // Deal with the control connection if it was using this host - Host ccHost = controlConnection.connectedHost(); - if (ccHost == null - || ccHost.equals(host) && loadBalancingPolicy().distance(ccHost) != HostDistance.LOCAL) - controlConnection.triggerReconnect(); - - for (SessionManager s : sessions) s.updateCreatedPools(host); - } - - private class ClusterCloseFuture extends CloseFuture.Forwarding { - - ClusterCloseFuture(List futures) { - super(futures); - } - - @Override - public CloseFuture force() { - // The only ExecutorService we haven't forced yet is executor - shutdownNow(executor); - return super.force(); - } - - @Override - protected void onFuturesDone() { - /* - * When we reach this, all sessions should be shutdown. We've also started a shutdown - * of the thread pools used by this object. Remains 2 things before marking the shutdown - * as done: - * 1) we need to wait for the completion of the shutdown of the Cluster threads pools. - * 2) we need to shutdown the Connection.Factory, i.e. the executors used by Netty. - * But at least for 2), we must not do it on the current thread because that could be - * a netty worker, which we're going to shutdown. So creates some thread for that. - */ - (new Thread("Shutdown-checker") { - @Override - public void run() { - // Just wait indefinitely on the the completion of the thread pools. Provided the - // user - // call force(), we'll never really block forever. - try { - if (reconnectionExecutor != null) { - reconnectionExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); - } - if (scheduledTasksExecutor != null) { - scheduledTasksExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); - } - if (executor != null) { - executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); - } - if (blockingExecutor != null) { - blockingExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); - } - - // Some of the jobs on the executors can be doing query stuff, so close the - // connectionFactory at the very last - if (connectionFactory != null) { - connectionFactory.shutdown(); - } - if (reaper != null) { - reaper.shutdown(); - } - set(null); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - setException(e); - } - } - }) - .start(); - } - } - - private class CleanupIdleConnectionsTask implements Runnable { - @Override - public void run() { - try { - long now = System.currentTimeMillis(); - for (SessionManager session : sessions) { - session.cleanupIdleConnections(now); - } - } catch (Exception e) { - logger.warn("Error while trashing idle connections", e); - } - } - } - - private class SchemaRefreshRequest { - - private final SchemaElement targetType; - private final String targetKeyspace; - private final String targetName; - private final List targetSignature; - - public SchemaRefreshRequest( - SchemaElement targetType, - String targetKeyspace, - String targetName, - List targetSignature) { - this.targetType = targetType; - this.targetKeyspace = Strings.emptyToNull(targetKeyspace); - this.targetName = Strings.emptyToNull(targetName); - this.targetSignature = targetSignature; - } - - /** - * Coalesce schema refresh requests. The algorithm is simple: if more than 2 keyspaces need - * refresh, then refresh the entire schema; otherwise if more than 2 elements in the same - * keyspace need refresh, then refresh the entire keyspace. - * - * @param that the other request to merge with the current one. - * @return A coalesced request - */ - SchemaRefreshRequest coalesce(SchemaRefreshRequest that) { - if (this.targetType == null || that.targetType == null) - return new SchemaRefreshRequest(null, null, null, null); - if (!this.targetKeyspace.equals(that.targetKeyspace)) - return new SchemaRefreshRequest(null, null, null, null); - if (this.targetName == null || that.targetName == null) - return new SchemaRefreshRequest(KEYSPACE, targetKeyspace, null, null); - if (!this.targetName.equals(that.targetName)) - return new SchemaRefreshRequest(KEYSPACE, targetKeyspace, null, null); - return this; - } - - @Override - public String toString() { - if (this.targetType == null) return "Refresh ALL"; - if (this.targetName == null) return "Refresh keyspace " + targetKeyspace; - return String.format("Refresh %s %s.%s", targetType, targetKeyspace, targetName); - } - } - - private class SchemaRefreshRequestDeliveryCallback - implements EventDebouncer.DeliveryCallback { - - @Override - public ListenableFuture deliver(final List events) { - return executor.submit( - new ExceptionCatchingRunnable() { - @Override - public void runMayThrow() throws InterruptedException, ExecutionException { - SchemaRefreshRequest coalesced = null; - for (SchemaRefreshRequest request : events) { - coalesced = coalesced == null ? request : coalesced.coalesce(request); - } - assert coalesced != null; - logger.trace("Coalesced schema refresh request: {}", coalesced); - controlConnection.refreshSchema( - coalesced.targetType, - coalesced.targetKeyspace, - coalesced.targetName, - coalesced.targetSignature); - } - }); - } - } - - private class NodeRefreshRequest { - - private final InetSocketAddress address; - - private final HostEvent eventType; - - private NodeRefreshRequest(InetSocketAddress address, HostEvent eventType) { - this.address = address; - this.eventType = eventType; - } - - @Override - public String toString() { - return address + " " + eventType; - } - } - - private class NodeRefreshRequestDeliveryCallback - implements EventDebouncer.DeliveryCallback { - - @Override - public ListenableFuture deliver(List events) { - Map hosts = new HashMap(); - // only keep the last event for each host - for (NodeRefreshRequest req : events) { - hosts.put(req.address, req.eventType); - } - List> futures = new ArrayList>(hosts.size()); - for (final Entry entry : hosts.entrySet()) { - InetSocketAddress address = entry.getKey(); - HostEvent eventType = entry.getValue(); - switch (eventType) { - case UP: - Host upHost = metadata.getHost(address); - if (upHost == null) { - // We don't have enough information to create a new Host (we are missing it's ID) - // so trigger a full node refresh - submitNodeListRefresh(); - } else { - futures.add(schedule(hostUp(upHost))); - } - break; - case DOWN: - // Note that there is a slight risk we can receive the event late and thus - // mark the host down even though we already had reconnected successfully. - // But it is unlikely, and don't have too much consequence since we'll try - // reconnecting - // right away, so we favor the detection to make the Host.isUp method more reliable. - Host downHost = metadata.getHost(address); - if (downHost != null) { - // Only process DOWN events if we have no active connections to the host . - // Otherwise, we - // wait for the connections to fail. This is to prevent against a bad control host - // aggressively marking DOWN all of its peers. - if (downHost.convictionPolicy.hasActiveConnections()) { - logger.debug( - "Ignoring down event on {} because it still has active connections", - downHost); - } else { - futures.add(execute(hostDown(downHost))); - } - } - break; - case REMOVED: - Host removedHost = metadata.getHost(address); - if (removedHost != null) futures.add(execute(hostRemoved(removedHost))); - break; - } - } - return Futures.allAsList(futures); - } - - private ListenableFuture execute(ExceptionCatchingRunnable task) { - return executor.submit(task); - } - - private ListenableFuture schedule(final ExceptionCatchingRunnable task) { - // Cassandra tends to send notifications for new/up nodes a bit early (it is triggered once - // gossip is up, but that is before the client-side server is up), so we add a delay - // (otherwise the connection will likely fail and have to be retry which is wasteful). - // This has been fixed by CASSANDRA-8236 and does not apply to protocol versions >= 4 - // and C* versions >= 2.2.0 - if (protocolVersion().compareTo(ProtocolVersion.V4) < 0) { - final SettableFuture future = SettableFuture.create(); - scheduledTasksExecutor.schedule( - new ExceptionCatchingRunnable() { - @Override - public void runMayThrow() throws Exception { - ListenableFuture f = execute(task); - GuavaCompatibility.INSTANCE.addCallback( - f, - new FutureCallback() { - @Override - public void onSuccess(Object result) { - future.set(null); - } - - @Override - public void onFailure(Throwable t) { - future.setException(t); - } - }); - } - }, - NEW_NODE_DELAY_SECONDS, - TimeUnit.SECONDS); - return future; - } else { - return execute(task); - } - } - - private ExceptionCatchingRunnable hostUp(final Host host) { - return new ExceptionCatchingRunnable() { - @Override - public void runMayThrow() throws Exception { - // Make sure we call controlConnection.refreshNodeInfo(host) - // so that we have up-to-date infos on that host before recreating the pools (so we - // typically catch that an upgraded node uses a new cassandra version). - if (controlConnection.refreshNodeInfo(host)) { - onUp(host, null); - } else { - logger.debug("Not enough info for {}, ignoring host", host); - } - } - }; - } - - private ExceptionCatchingRunnable hostDown(final Host host) { - return new ExceptionCatchingRunnable() { - @Override - public void runMayThrow() throws Exception { - onDown(host, false, true); - } - }; - } - - private ExceptionCatchingRunnable hostRemoved(final Host host) { - return new ExceptionCatchingRunnable() { - @Override - public void runMayThrow() throws Exception { - if (metadata.remove(host)) { - logger.info("Cassandra host {} removed", host); - onRemove(host); - submitNodeListRefresh(); - } - } - }; - } - } - - private class NodeListRefreshRequest { - @Override - public String toString() { - return "Refresh node list and token map"; - } - } - - private class NodeListRefreshRequestDeliveryCallback - implements EventDebouncer.DeliveryCallback { - - @Override - public ListenableFuture deliver(List events) { - // The number of received requests does not matter - // as long as one request is made, refresh the entire node list - return executor.submit( - new ExceptionCatchingRunnable() { - @Override - public void runMayThrow() throws InterruptedException, ExecutionException { - controlConnection.refreshNodeListAndTokenMap(); - } - }); - } - } - } - - private enum HostEvent { - UP, - DOWN, - REMOVED - } - - /** - * Periodically ensures that closed connections are properly terminated once they have no more - * pending requests. - * - *

This is normally done when the connection errors out, or when the last request is processed; - * this class acts as a last-effort protection since unterminated connections can lead to - * deadlocks. If it terminates a connection, this indicates a bug; warnings are logged so that - * this can be reported. - * - * @see Connection#tryTerminate(boolean) - */ - static class ConnectionReaper { - private static final int INTERVAL_MS = 15000; - - private final ScheduledExecutorService executor; - - @VisibleForTesting - final Map connections = new ConcurrentHashMap(); - - private volatile boolean shutdown; - - private final Runnable reaperTask = - new Runnable() { - @Override - public void run() { - long now = System.currentTimeMillis(); - Iterator> iterator = connections.entrySet().iterator(); - while (iterator.hasNext()) { - Entry entry = iterator.next(); - Connection connection = entry.getKey(); - Long terminateTime = entry.getValue(); - if (terminateTime <= now) { - boolean terminated = connection.tryTerminate(true); - if (terminated) iterator.remove(); - } - } - } - }; - - ConnectionReaper(ScheduledExecutorService executor) { - this.executor = executor; - this.executor.scheduleWithFixedDelay( - reaperTask, INTERVAL_MS, INTERVAL_MS, TimeUnit.MILLISECONDS); - } - - void register(Connection connection, long terminateTime) { - if (shutdown) { - // This should not happen since the reaper is shut down after all sessions. - logger.warn("Connection registered after reaper shutdown: {}", connection); - connection.tryTerminate(true); - } else { - connections.put(connection, terminateTime); - } - } - - void shutdown() { - shutdown = true; - // Force shutdown to avoid waiting for the interval, and run the task manually one last time - executor.shutdownNow(); - reaperTask.run(); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ClusterNameMismatchException.java b/driver-core/src/main/java/com/datastax/driver/core/ClusterNameMismatchException.java deleted file mode 100644 index 31891be7761..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ClusterNameMismatchException.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Indicates that we've attempted to connect to a node which cluster name doesn't match that of the - * other nodes known to the driver. - */ -class ClusterNameMismatchException extends Exception { - - private static final long serialVersionUID = 0; - - public final EndPoint endPoint; - public final String expectedClusterName; - public final String actualClusterName; - - public ClusterNameMismatchException( - EndPoint endPoint, String actualClusterName, String expectedClusterName) { - super( - String.format( - "[%s] Host %s reports cluster name '%s' that doesn't match our cluster name '%s'. This host will be ignored.", - endPoint, endPoint, actualClusterName, expectedClusterName)); - this.endPoint = endPoint; - this.expectedClusterName = expectedClusterName; - this.actualClusterName = actualClusterName; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ClusterWidePercentileTracker.java b/driver-core/src/main/java/com/datastax/driver/core/ClusterWidePercentileTracker.java deleted file mode 100644 index bc0dd7ecd37..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ClusterWidePercentileTracker.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * A {@code PercentileTracker} that aggregates all measurements into a single histogram. - * - *

This gives you global latency percentiles for the whole cluster, meaning that latencies of - * slower hosts will tend to appear in higher percentiles. - */ -public class ClusterWidePercentileTracker extends PercentileTracker { - private volatile Cluster cluster; - - private ClusterWidePercentileTracker( - long highestTrackableLatencyMillis, - int numberOfSignificantValueDigits, - int minRecordedValues, - long intervalMs) { - super( - highestTrackableLatencyMillis, - numberOfSignificantValueDigits, - minRecordedValues, - intervalMs); - } - - @Override - public void onRegister(Cluster cluster) { - this.cluster = cluster; - } - - @Override - protected Cluster computeKey(Host host, Statement statement, Exception exception) { - return cluster; - } - - /** - * Returns a builder to create a new instance. - * - * @param highestTrackableLatencyMillis the highest expected latency. If a higher value is - * reported, it will be ignored and a warning will be logged. A good rule of thumb is to set - * it slightly higher than {@link SocketOptions#getReadTimeoutMillis()}. - * @return the builder. - */ - public static Builder builder(long highestTrackableLatencyMillis) { - return new Builder(highestTrackableLatencyMillis); - } - - /** Helper class to build {@code PerHostPercentileTracker} instances with a fluent interface. */ - public static class Builder - extends PercentileTracker.Builder { - - Builder(long highestTrackableLatencyMillis) { - super(highestTrackableLatencyMillis); - } - - @Override - protected Builder self() { - return this; - } - - @Override - public ClusterWidePercentileTracker build() { - return new ClusterWidePercentileTracker( - highestTrackableLatencyMillis, - numberOfSignificantValueDigits, - minRecordedValues, - intervalMs); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java b/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java deleted file mode 100644 index 701d2e9a09a..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/CodecRegistry.java +++ /dev/null @@ -1,812 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.DataType.Name.LIST; -import static com.datastax.driver.core.DataType.Name.MAP; -import static com.datastax.driver.core.DataType.Name.SET; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.datastax.driver.core.exceptions.CodecNotFoundException; -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.cache.CacheBuilder; -import com.google.common.cache.CacheLoader; -import com.google.common.cache.LoadingCache; -import com.google.common.cache.RemovalListener; -import com.google.common.cache.RemovalNotification; -import com.google.common.cache.Weigher; -import com.google.common.reflect.TypeToken; -import com.google.common.util.concurrent.UncheckedExecutionException; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.nio.ByteBuffer; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutionException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A registry for {@link TypeCodec}s. When the driver needs to serialize or deserialize a Java type - * to/from CQL, it will lookup in the registry for a suitable codec. The registry is initialized - * with default codecs that handle basic conversions (e.g. CQL {@code text} to {@code - * java.lang.String}), and users can add their own. Complex codecs can also be generated on-the-fly - * from simpler ones (more details below). - * - *

Creating a registry

- * - * By default, the driver uses {@link CodecRegistry#DEFAULT_INSTANCE}, a shareable, JVM-wide - * instance initialized with built-in codecs for all the base CQL types. The only reason to create - * your own instances is if you have multiple {@code Cluster} objects that use different sets of - * codecs. In that case, use {@link - * com.datastax.driver.core.Cluster.Builder#withCodecRegistry(CodecRegistry)} to associate the - * registry with the cluster: - * - *
{@code
- * CodecRegistry myCodecRegistry = new CodecRegistry();
- * myCodecRegistry.register(myCodec1, myCodec2, myCodec3);
- * Cluster cluster = Cluster.builder().withCodecRegistry(myCodecRegistry).build();
- *
- * // To retrieve the registry later:
- * CodecRegistry registry = cluster.getConfiguration().getCodecRegistry();
- * }
- * - * {@code CodecRegistry} instances are thread-safe. - * - *

It is possible to turn on log messages by setting the {@code - * com.datastax.driver.core.CodecRegistry} logger level to {@code TRACE}. Beware that the registry - * can be very verbose at this log level. - * - *

Registering and using custom codecs

- * - * To create a custom codec, write a class that extends {@link TypeCodec}, create an instance, and - * pass it to one of the {@link #register(TypeCodec) register} methods; for example, one could - * create a codec that maps CQL timestamps to JDK8's {@code java.time.LocalDate}: - * - *
{@code
- * class LocalDateCodec extends TypeCodec {
- *    ...
- * }
- * myCodecRegistry.register(new LocalDateCodec());
- * }
- * - * The conversion will be available to: - * - *
    - *
  • all driver types that implement {@link GettableByIndexData}, {@link GettableByNameData}, - * {@link SettableByIndexData} and/or {@link SettableByNameData}. Namely: {@link Row}, {@link - * BoundStatement}, {@link UDTValue} and {@link TupleValue}; - *
  • {@link SimpleStatement#SimpleStatement(String, Object...) simple statements}; - *
  • statements created with the {@link com.datastax.driver.core.querybuilder.QueryBuilder Query - * builder}. - *
- * - *

Example: - * - *

{@code
- * Row row = session.executeQuery("select date from some_table where pk = 1").one();
- * java.time.LocalDate date = row.get(0, java.time.LocalDate.class); // uses LocalDateCodec registered above
- * }
- * - * You can also bypass the codec registry by passing a standalone codec instance to methods such as - * {@link GettableByIndexData#get(int, TypeCodec)}. - * - *

Codec generation

- * - * When a {@code CodecRegistry} cannot find a suitable codec among existing ones, it will attempt to - * create it on-the-fly. It can manage: - * - *
    - *
  • collections (lists, sets and maps) of known types. For example, if you registered a codec - * for JDK8's {@code java.time.LocalDate} like in the example above, you get {@code - * List>} and {@code Set>} handled for free, as well as all {@code Map} - * types whose keys and/or values are {@code java.time.LocalDate}. This works recursively for - * nested collections; - *
  • {@link UserType user types}, mapped to {@link UDTValue} objects. Custom codecs are - * available recursively to the UDT's fields, so if one of your fields is a {@code timestamp} - * you can use your {@code LocalDateCodec} to retrieve it as a {@code java.time.LocalDate}; - *
  • {@link TupleType tuple types}, mapped to {@link TupleValue} (with the same rules for nested - * fields); - *
  • {@link com.datastax.driver.core.DataType.CustomType custom types}, mapped to {@code - * ByteBuffer}. - *
- * - * If the codec registry encounters a mapping that it can't handle automatically, a {@link - * CodecNotFoundException} is thrown; you'll need to register a custom codec for it. - * - *

Performance and caching

- * - * Whenever possible, the registry will cache the result of a codec lookup for a specific type - * mapping, including any generated codec. For example, if you registered {@code LocalDateCodec} and - * ask the registry for a codec to convert a CQL {@code list} to a Java {@code - * List}: - * - *
    - *
  1. the first lookup will generate a {@code TypeCodec>} from {@code - * LocalDateCodec}, and put it in the cache; - *
  2. the second lookup will hit the cache directly, and reuse the previously generated instance. - *
- * - * The javadoc for each {@link #codecFor(DataType) codecFor} variant specifies whether the result - * can be cached or not. - * - *

Codec order

- * - * When the registry looks up a codec, the rules of precedence are: - * - *
    - *
  • if a result was previously cached for that mapping, it is returned; - *
  • otherwise, the registry checks the list of built-in codecs – the default ones – and the - * ones that were explicitly registered (in the order that they were registered). It calls - * each codec's {@code accepts} methods to determine if it can handle the mapping, and if so - * returns it; - *
  • otherwise, the registry tries to generate a codec, according to the rules outlined above. - *
- * - * It is currently impossible to override an existing codec. If you try to do so, {@link - * #register(TypeCodec)} will log a warning and ignore it. - */ -public final class CodecRegistry { - - private static final Logger logger = LoggerFactory.getLogger(CodecRegistry.class); - - private static final Map> BUILT_IN_CODECS_MAP = - new EnumMap>(DataType.Name.class); - - static { - BUILT_IN_CODECS_MAP.put(DataType.Name.ASCII, TypeCodec.ascii()); - BUILT_IN_CODECS_MAP.put(DataType.Name.BIGINT, TypeCodec.bigint()); - BUILT_IN_CODECS_MAP.put(DataType.Name.BLOB, TypeCodec.blob()); - BUILT_IN_CODECS_MAP.put(DataType.Name.BOOLEAN, TypeCodec.cboolean()); - BUILT_IN_CODECS_MAP.put(DataType.Name.COUNTER, TypeCodec.counter()); - BUILT_IN_CODECS_MAP.put(DataType.Name.DECIMAL, TypeCodec.decimal()); - BUILT_IN_CODECS_MAP.put(DataType.Name.DOUBLE, TypeCodec.cdouble()); - BUILT_IN_CODECS_MAP.put(DataType.Name.FLOAT, TypeCodec.cfloat()); - BUILT_IN_CODECS_MAP.put(DataType.Name.INET, TypeCodec.inet()); - BUILT_IN_CODECS_MAP.put(DataType.Name.INT, TypeCodec.cint()); - BUILT_IN_CODECS_MAP.put(DataType.Name.TEXT, TypeCodec.varchar()); - BUILT_IN_CODECS_MAP.put(DataType.Name.TIMESTAMP, TypeCodec.timestamp()); - BUILT_IN_CODECS_MAP.put(DataType.Name.UUID, TypeCodec.uuid()); - BUILT_IN_CODECS_MAP.put(DataType.Name.VARCHAR, TypeCodec.varchar()); - BUILT_IN_CODECS_MAP.put(DataType.Name.VARINT, TypeCodec.varint()); - BUILT_IN_CODECS_MAP.put(DataType.Name.TIMEUUID, TypeCodec.timeUUID()); - BUILT_IN_CODECS_MAP.put(DataType.Name.SMALLINT, TypeCodec.smallInt()); - BUILT_IN_CODECS_MAP.put(DataType.Name.TINYINT, TypeCodec.tinyInt()); - BUILT_IN_CODECS_MAP.put(DataType.Name.DATE, TypeCodec.date()); - BUILT_IN_CODECS_MAP.put(DataType.Name.TIME, TypeCodec.time()); - BUILT_IN_CODECS_MAP.put(DataType.Name.DURATION, TypeCodec.duration()); - } - - // roughly sorted by popularity - private static final TypeCodec[] BUILT_IN_CODECS = - new TypeCodec[] { - TypeCodec - .varchar(), // must be declared before AsciiCodec so it gets chosen when CQL type not - // available - TypeCodec - .uuid(), // must be declared before TimeUUIDCodec so it gets chosen when CQL type not - // available - TypeCodec.timeUUID(), - TypeCodec.timestamp(), - TypeCodec.cint(), - TypeCodec.bigint(), - TypeCodec.blob(), - TypeCodec.cdouble(), - TypeCodec.cfloat(), - TypeCodec.decimal(), - TypeCodec.varint(), - TypeCodec.inet(), - TypeCodec.cboolean(), - TypeCodec.smallInt(), - TypeCodec.tinyInt(), - TypeCodec.date(), - TypeCodec.time(), - TypeCodec.duration(), - TypeCodec.counter(), - TypeCodec.ascii() - }; - - /** - * The default {@code CodecRegistry} instance. - * - *

It will be shared among all {@link Cluster} instances that were not explicitly built with a - * different instance. - */ - public static final CodecRegistry DEFAULT_INSTANCE = new CodecRegistry(); - - /** Cache key for the codecs cache. */ - private static final class CacheKey { - - private final DataType cqlType; - - private final TypeToken javaType; - - CacheKey(DataType cqlType, TypeToken javaType) { - this.javaType = javaType; - this.cqlType = cqlType; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CacheKey cacheKey = (CacheKey) o; - return MoreObjects.equal(cqlType, cacheKey.cqlType) - && MoreObjects.equal(javaType, cacheKey.javaType); - } - - @Override - public int hashCode() { - return MoreObjects.hashCode(cqlType, javaType); - } - } - - /** Cache loader for the codecs cache. */ - private class TypeCodecCacheLoader extends CacheLoader> { - @Override - public TypeCodec load(CacheKey cacheKey) { - checkNotNull(cacheKey.cqlType, "Parameter cqlType cannot be null"); - if (logger.isTraceEnabled()) - logger.trace( - "Loading codec into cache: [{} <-> {}]", - CodecRegistry.toString(cacheKey.cqlType), - CodecRegistry.toString(cacheKey.javaType)); - for (TypeCodec codec : codecs) { - if (codec.accepts(cacheKey.cqlType) - && (cacheKey.javaType == null || codec.accepts(cacheKey.javaType))) { - logger.trace("Already existing codec found: {}", codec); - return codec; - } - } - return createCodec(cacheKey.cqlType, cacheKey.javaType); - } - } - - /** - * A complexity-based weigher for the codecs cache. Weights are computed mainly according to the - * CQL type: - * - *

    - *
  1. Manually-registered codecs always weigh 0; - *
  2. Codecs for primitive types weigh 0; - *
  3. Codecs for collections weigh the total weight of their inner types + the weight of their - * level of deepness; - *
  4. Codecs for UDTs and tuples weigh the total weight of their inner types + the weight of - * their level of deepness, but cannot weigh less than 1; - *
  5. Codecs for custom (non-CQL) types weigh 1. - *
- * - * A consequence of this algorithm is that codecs for primitive types and codecs for all "shallow" - * collections thereof are never evicted. - */ - private class TypeCodecWeigher implements Weigher> { - - @Override - public int weigh(CacheKey key, TypeCodec value) { - return codecs.contains(value) ? 0 : weigh(value.cqlType, 0); - } - - private int weigh(DataType cqlType, int level) { - switch (cqlType.getName()) { - case LIST: - case SET: - case MAP: - { - int weight = level; - for (DataType eltType : cqlType.getTypeArguments()) { - weight += weigh(eltType, level + 1); - } - return weight; - } - case UDT: - { - int weight = level; - for (UserType.Field field : ((UserType) cqlType)) { - weight += weigh(field.getType(), level + 1); - } - return weight == 0 ? 1 : weight; - } - case TUPLE: - { - int weight = level; - for (DataType componentType : ((TupleType) cqlType).getComponentTypes()) { - weight += weigh(componentType, level + 1); - } - return weight == 0 ? 1 : weight; - } - case CUSTOM: - return 1; - default: - return 0; - } - } - } - - /** - * Simple removal listener for the codec cache (can be used for debugging purposes by setting the - * {@code com.datastax.driver.core.CodecRegistry} logger level to {@code TRACE}. - */ - private class TypeCodecRemovalListener implements RemovalListener> { - @Override - public void onRemoval(RemovalNotification> notification) { - logger.trace( - "Evicting codec from cache: {} (cause: {})", - notification.getValue(), - notification.getCause()); - } - } - - /** The list of user-registered codecs. */ - private final CopyOnWriteArrayList> codecs; - - /** - * A LoadingCache to serve requests for codecs whenever possible. The cache can be used as long as - * at least the CQL type is known. - */ - private final LoadingCache> cache; - - /** Creates a new instance initialized with built-in codecs for all the base CQL types. */ - public CodecRegistry() { - this.codecs = new CopyOnWriteArrayList>(); - this.cache = defaultCacheBuilder().build(new TypeCodecCacheLoader()); - } - - private CacheBuilder> defaultCacheBuilder() { - CacheBuilder> builder = - CacheBuilder.newBuilder() - // lists, sets and maps of 20 primitive types = 20 + 20 + 20*20 = 440 codecs, - // so let's start with roughly 1/4 of that - .initialCapacity(100) - .maximumWeight(1000) - .weigher(new TypeCodecWeigher()); - if (logger.isTraceEnabled()) - // do not bother adding a listener if it will be ineffective - builder = builder.removalListener(new TypeCodecRemovalListener()); - return builder; - } - - /** - * Register the given codec with this registry. - * - *

This method will log a warning and ignore the codec if it collides with a previously - * registered one. Note that this check is not done in a completely thread-safe manner; codecs - * should typically be registered at application startup, not in a highly concurrent context (if a - * race condition occurs, the worst possible outcome is that no warning gets logged, and the codec - * gets registered but will never actually be used). - * - * @param newCodec The codec to add to the registry. - * @return this CodecRegistry (for method chaining). - */ - public CodecRegistry register(TypeCodec newCodec) { - for (TypeCodec oldCodec : BUILT_IN_CODECS) { - if (oldCodec.accepts(newCodec.getCqlType()) && oldCodec.accepts(newCodec.getJavaType())) { - logger.warn( - "Ignoring codec {} because it collides with previously registered codec {}", - newCodec, - oldCodec); - return this; - } - } - for (TypeCodec oldCodec : codecs) { - if (oldCodec.accepts(newCodec.getCqlType()) && oldCodec.accepts(newCodec.getJavaType())) { - logger.warn( - "Ignoring codec {} because it collides with previously registered codec {}", - newCodec, - oldCodec); - return this; - } - } - CacheKey key = new CacheKey(newCodec.getCqlType(), newCodec.getJavaType()); - TypeCodec existing = cache.getIfPresent(key); - if (existing != null) { - logger.warn( - "Ignoring codec {} because it collides with previously generated codec {}", - newCodec, - existing); - return this; - } - this.codecs.add(newCodec); - return this; - } - - /** - * Register the given codecs with this registry. - * - * @param codecs The codecs to add to the registry. - * @return this CodecRegistry (for method chaining). - * @see #register(TypeCodec) - */ - public CodecRegistry register(TypeCodec... codecs) { - for (TypeCodec codec : codecs) register(codec); - return this; - } - - /** - * Register the given codecs with this registry. - * - * @param codecs The codecs to add to the registry. - * @return this CodecRegistry (for method chaining). - * @see #register(TypeCodec) - */ - public CodecRegistry register(Iterable> codecs) { - for (TypeCodec codec : codecs) register(codec); - return this; - } - - /** - * Returns a {@link TypeCodec codec} that accepts the given value. - * - *

This method takes an arbitrary Java object and tries to locate a suitable codec for it. - * Codecs must perform a {@link TypeCodec#accepts(Object) runtime inspection} of the object to - * determine if they can accept it or not, which, depending on the implementations, can be - * expensive; besides, the resulting codec cannot be cached. Therefore there might be a - * performance penalty when using this method. - * - *

Furthermore, this method returns the first matching codec, regardless of its accepted CQL - * type. It should be reserved for situations where the target CQL type is not available or - * unknown. In the Java driver, this happens mainly when serializing a value in a {@link - * SimpleStatement#SimpleStatement(String, Object...) SimpleStatement} or in the {@link - * com.datastax.driver.core.querybuilder.QueryBuilder}, where no CQL type information is - * available. - * - *

Codecs returned by this method are NOT cached (see the {@link CodecRegistry - * top-level documentation} of this class for more explanations about caching). - * - * @param value The value the codec should accept; must not be {@code null}. - * @return A suitable codec. - * @throws CodecNotFoundException if a suitable codec cannot be found. - */ - public TypeCodec codecFor(T value) { - return findCodec(null, value); - } - - /** - * Returns a {@link TypeCodec codec} that accepts the given {@link DataType CQL type}. - * - *

This method returns the first matching codec, regardless of its accepted Java type. It - * should be reserved for situations where the Java type is not available or unknown. In the Java - * driver, this happens mainly when deserializing a value using the {@link - * GettableByIndexData#getObject(int) getObject} method. - * - *

Codecs returned by this method are cached (see the {@link CodecRegistry top-level - * documentation} of this class for more explanations about caching). - * - * @param cqlType The {@link DataType CQL type} the codec should accept; must not be {@code null}. - * @return A suitable codec. - * @throws CodecNotFoundException if a suitable codec cannot be found. - */ - public TypeCodec codecFor(DataType cqlType) throws CodecNotFoundException { - return lookupCodec(cqlType, null); - } - - /** - * Returns a {@link TypeCodec codec} that accepts the given {@link DataType CQL type} and the - * given Java class. - * - *

This method can only handle raw (non-parameterized) Java types. For parameterized types, use - * {@link #codecFor(DataType, TypeToken)} instead. - * - *

Codecs returned by this method are cached (see the {@link CodecRegistry top-level - * documentation} of this class for more explanations about caching). - * - * @param cqlType The {@link DataType CQL type} the codec should accept; must not be {@code null}. - * @param javaType The Java type the codec should accept; can be {@code null}. - * @return A suitable codec. - * @throws CodecNotFoundException if a suitable codec cannot be found. - */ - public TypeCodec codecFor(DataType cqlType, Class javaType) - throws CodecNotFoundException { - return codecFor(cqlType, TypeToken.of(javaType)); - } - - /** - * Returns a {@link TypeCodec codec} that accepts the given {@link DataType CQL type} and the - * given Java type. - * - *

This method handles parameterized types thanks to Guava's {@link TypeToken} API. - * - *

Codecs returned by this method are cached (see the {@link CodecRegistry top-level - * documentation} of this class for more explanations about caching). - * - * @param cqlType The {@link DataType CQL type} the codec should accept; must not be {@code null}. - * @param javaType The {@link TypeToken Java type} the codec should accept; can be {@code null}. - * @return A suitable codec. - * @throws CodecNotFoundException if a suitable codec cannot be found. - */ - public TypeCodec codecFor(DataType cqlType, TypeToken javaType) - throws CodecNotFoundException { - return lookupCodec(cqlType, javaType); - } - - /** - * Returns a {@link TypeCodec codec} that accepts the given {@link DataType CQL type} and the - * given value. - * - *

This method takes an arbitrary Java object and tries to locate a suitable codec for it. - * Codecs must perform a {@link TypeCodec#accepts(Object) runtime inspection} of the object to - * determine if they can accept it or not, which, depending on the implementations, can be - * expensive; besides, the resulting codec cannot be cached. Therefore there might be a - * performance penalty when using this method. - * - *

Codecs returned by this method are NOT cached (see the {@link CodecRegistry - * top-level documentation} of this class for more explanations about caching). - * - * @param cqlType The {@link DataType CQL type} the codec should accept; can be {@code null}. - * @param value The value the codec should accept; must not be {@code null}. - * @return A suitable codec. - * @throws CodecNotFoundException if a suitable codec cannot be found. - */ - public TypeCodec codecFor(DataType cqlType, T value) { - return findCodec(cqlType, value); - } - - @SuppressWarnings("unchecked") - private TypeCodec lookupCodec(DataType cqlType, TypeToken javaType) { - checkNotNull(cqlType, "Parameter cqlType cannot be null"); - TypeCodec codec = BUILT_IN_CODECS_MAP.get(cqlType.getName()); - if (codec != null && (javaType == null || codec.accepts(javaType))) { - logger.trace("Returning built-in codec {}", codec); - return (TypeCodec) codec; - } - if (logger.isTraceEnabled()) - logger.trace("Querying cache for codec [{} <-> {}]", toString(cqlType), toString(javaType)); - try { - CacheKey cacheKey = new CacheKey(cqlType, javaType); - codec = cache.get(cacheKey); - } catch (UncheckedExecutionException e) { - if (e.getCause() instanceof CodecNotFoundException) { - throw (CodecNotFoundException) e.getCause(); - } - throw new CodecNotFoundException(e.getCause(), cqlType, javaType); - } catch (RuntimeException e) { - throw new CodecNotFoundException(e.getCause(), cqlType, javaType); - } catch (ExecutionException e) { - throw new CodecNotFoundException(e.getCause(), cqlType, javaType); - } - logger.trace("Returning cached codec {}", codec); - return (TypeCodec) codec; - } - - @SuppressWarnings("unchecked") - private TypeCodec findCodec(DataType cqlType, TypeToken javaType) { - checkNotNull(cqlType, "Parameter cqlType cannot be null"); - if (logger.isTraceEnabled()) - logger.trace("Looking for codec [{} <-> {}]", toString(cqlType), toString(javaType)); - - // Look at the built-in codecs first - for (TypeCodec codec : BUILT_IN_CODECS) { - if (codec.accepts(cqlType) && (javaType == null || codec.accepts(javaType))) { - logger.trace("Built-in codec found: {}", codec); - return (TypeCodec) codec; - } - } - - // Look at the user-registered codecs next - for (TypeCodec codec : codecs) { - if (codec.accepts(cqlType) && (javaType == null || codec.accepts(javaType))) { - logger.trace("Already registered codec found: {}", codec); - return (TypeCodec) codec; - } - } - return createCodec(cqlType, javaType); - } - - @SuppressWarnings("unchecked") - private TypeCodec findCodec(DataType cqlType, T value) { - checkNotNull(value, "Parameter value cannot be null"); - if (logger.isTraceEnabled()) - logger.trace("Looking for codec [{} <-> {}]", toString(cqlType), value.getClass()); - - // Look at the built-in codecs first - for (TypeCodec codec : BUILT_IN_CODECS) { - if ((cqlType == null || codec.accepts(cqlType)) && codec.accepts(value)) { - logger.trace("Built-in codec found: {}", codec); - return (TypeCodec) codec; - } - } - - // Look at the user-registered codecs next - for (TypeCodec codec : codecs) { - if ((cqlType == null || codec.accepts(cqlType)) && codec.accepts(value)) { - logger.trace("Already registered codec found: {}", codec); - return (TypeCodec) codec; - } - } - return createCodec(cqlType, value); - } - - private TypeCodec createCodec(DataType cqlType, TypeToken javaType) { - TypeCodec codec = maybeCreateCodec(cqlType, javaType); - if (codec == null) throw notFound(cqlType, javaType); - // double-check that the created codec satisfies the initial request - // this check can fail specially when creating codecs for collections - // e.g. if B extends A and there is a codec registered for A and - // we request a codec for List, the registry would generate a codec for List - if (!codec.accepts(cqlType) || (javaType != null && !codec.accepts(javaType))) - throw notFound(cqlType, javaType); - logger.trace("Codec created: {}", codec); - return codec; - } - - private TypeCodec createCodec(DataType cqlType, T value) { - TypeCodec codec = maybeCreateCodec(cqlType, value); - if (codec == null) throw notFound(cqlType, TypeToken.of(value.getClass())); - // double-check that the created codec satisfies the initial request - if ((cqlType != null && !codec.accepts(cqlType)) || !codec.accepts(value)) - throw notFound(cqlType, TypeToken.of(value.getClass())); - logger.trace("Codec created: {}", codec); - return codec; - } - - @SuppressWarnings("unchecked") - private TypeCodec maybeCreateCodec(DataType cqlType, TypeToken javaType) { - checkNotNull(cqlType); - - if (cqlType.getName() == LIST - && (javaType == null || List.class.isAssignableFrom(javaType.getRawType()))) { - TypeToken elementType = null; - if (javaType != null && javaType.getType() instanceof ParameterizedType) { - Type[] typeArguments = ((ParameterizedType) javaType.getType()).getActualTypeArguments(); - elementType = TypeToken.of(typeArguments[0]); - } - TypeCodec eltCodec = findCodec(cqlType.getTypeArguments().get(0), elementType); - return (TypeCodec) TypeCodec.list(eltCodec); - } - - if (cqlType.getName() == SET - && (javaType == null || Set.class.isAssignableFrom(javaType.getRawType()))) { - TypeToken elementType = null; - if (javaType != null && javaType.getType() instanceof ParameterizedType) { - Type[] typeArguments = ((ParameterizedType) javaType.getType()).getActualTypeArguments(); - elementType = TypeToken.of(typeArguments[0]); - } - TypeCodec eltCodec = findCodec(cqlType.getTypeArguments().get(0), elementType); - return (TypeCodec) TypeCodec.set(eltCodec); - } - - if (cqlType.getName() == MAP - && (javaType == null || Map.class.isAssignableFrom(javaType.getRawType()))) { - TypeToken keyType = null; - TypeToken valueType = null; - if (javaType != null && javaType.getType() instanceof ParameterizedType) { - Type[] typeArguments = ((ParameterizedType) javaType.getType()).getActualTypeArguments(); - keyType = TypeToken.of(typeArguments[0]); - valueType = TypeToken.of(typeArguments[1]); - } - TypeCodec keyCodec = findCodec(cqlType.getTypeArguments().get(0), keyType); - TypeCodec valueCodec = findCodec(cqlType.getTypeArguments().get(1), valueType); - return (TypeCodec) TypeCodec.map(keyCodec, valueCodec); - } - - if (cqlType instanceof TupleType - && (javaType == null || TupleValue.class.isAssignableFrom(javaType.getRawType()))) { - return (TypeCodec) TypeCodec.tuple((TupleType) cqlType); - } - - if (cqlType instanceof UserType - && (javaType == null || UDTValue.class.isAssignableFrom(javaType.getRawType()))) { - return (TypeCodec) TypeCodec.userType((UserType) cqlType); - } - - if (cqlType instanceof DataType.CustomType - && (javaType == null || ByteBuffer.class.isAssignableFrom(javaType.getRawType()))) { - return (TypeCodec) TypeCodec.custom((DataType.CustomType) cqlType); - } - - return null; - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - private TypeCodec maybeCreateCodec(DataType cqlType, T value) { - checkNotNull(value); - - if ((cqlType == null || cqlType.getName() == LIST) && value instanceof List) { - List list = (List) value; - if (list.isEmpty()) { - DataType elementType = - (cqlType == null || cqlType.getTypeArguments().isEmpty()) - ? DataType.blob() - : cqlType.getTypeArguments().get(0); - return TypeCodec.list(findCodec(elementType, (TypeToken) null)); - } else { - DataType elementType = - (cqlType == null || cqlType.getTypeArguments().isEmpty()) - ? null - : cqlType.getTypeArguments().get(0); - return (TypeCodec) TypeCodec.list(findCodec(elementType, list.iterator().next())); - } - } - - if ((cqlType == null || cqlType.getName() == SET) && value instanceof Set) { - Set set = (Set) value; - if (set.isEmpty()) { - DataType elementType = - (cqlType == null || cqlType.getTypeArguments().isEmpty()) - ? DataType.blob() - : cqlType.getTypeArguments().get(0); - return TypeCodec.set(findCodec(elementType, (TypeToken) null)); - } else { - DataType elementType = - (cqlType == null || cqlType.getTypeArguments().isEmpty()) - ? null - : cqlType.getTypeArguments().get(0); - return (TypeCodec) TypeCodec.set(findCodec(elementType, set.iterator().next())); - } - } - - if ((cqlType == null || cqlType.getName() == MAP) && value instanceof Map) { - Map map = (Map) value; - if (map.isEmpty()) { - DataType keyType = - (cqlType == null || cqlType.getTypeArguments().size() < 1) - ? DataType.blob() - : cqlType.getTypeArguments().get(0); - DataType valueType = - (cqlType == null || cqlType.getTypeArguments().size() < 2) - ? DataType.blob() - : cqlType.getTypeArguments().get(1); - return TypeCodec.map( - findCodec(keyType, (TypeToken) null), findCodec(valueType, (TypeToken) null)); - } else { - DataType keyType = - (cqlType == null || cqlType.getTypeArguments().size() < 1) - ? null - : cqlType.getTypeArguments().get(0); - DataType valueType = - (cqlType == null || cqlType.getTypeArguments().size() < 2) - ? null - : cqlType.getTypeArguments().get(1); - Map.Entry entry = (Map.Entry) map.entrySet().iterator().next(); - return (TypeCodec) - TypeCodec.map( - findCodec(keyType, entry.getKey()), findCodec(valueType, entry.getValue())); - } - } - - if ((cqlType == null || cqlType.getName() == DataType.Name.TUPLE) - && value instanceof TupleValue) { - return (TypeCodec) - TypeCodec.tuple(cqlType == null ? ((TupleValue) value).getType() : (TupleType) cqlType); - } - - if ((cqlType == null || cqlType.getName() == DataType.Name.UDT) && value instanceof UDTValue) { - return (TypeCodec) - TypeCodec.userType(cqlType == null ? ((UDTValue) value).getType() : (UserType) cqlType); - } - - if ((cqlType != null && cqlType instanceof DataType.CustomType) - && value instanceof ByteBuffer) { - return (TypeCodec) TypeCodec.custom((DataType.CustomType) cqlType); - } - - return null; - } - - private static CodecNotFoundException notFound(DataType cqlType, TypeToken javaType) { - String msg = - String.format( - "Codec not found for requested operation: [%s <-> %s]", - toString(cqlType), toString(javaType)); - return new CodecNotFoundException(msg, cqlType, javaType); - } - - private static String toString(Object value) { - return value == null ? "ANY" : value.toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/CodecUtils.java b/driver-core/src/main/java/com/datastax/driver/core/CodecUtils.java deleted file mode 100644 index afaa7176bca..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/CodecUtils.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.nio.ByteBuffer; - -/** A set of utility methods to deal with type conversion and serialization. */ -public final class CodecUtils { - - private static final long MAX_CQL_LONG_VALUE = ((1L << 32) - 1); - - private static final long EPOCH_AS_CQL_LONG = (1L << 31); - - private CodecUtils() {} - - /** - * Utility method that "packs" together a list of {@link ByteBuffer}s containing serialized - * collection elements. Mainly intended for use with collection codecs when serializing - * collections. - * - * @param buffers the collection elements - * @param elements the total number of elements - * @param version the protocol version to use - * @return The serialized collection - */ - public static ByteBuffer pack(ByteBuffer[] buffers, int elements, ProtocolVersion version) { - int size = 0; - for (ByteBuffer bb : buffers) { - int elemSize = sizeOfValue(bb, version); - size += elemSize; - } - ByteBuffer result = ByteBuffer.allocate(sizeOfCollectionSize(version) + size); - writeSize(result, elements, version); - for (ByteBuffer bb : buffers) writeValue(result, bb, version); - return (ByteBuffer) result.flip(); - } - - /** - * Utility method that reads a size value. Mainly intended for collection codecs when - * deserializing CQL collections. - * - * @param input The ByteBuffer to read from. - * @param version The protocol version to use. - * @return The size value. - */ - public static int readSize(ByteBuffer input, ProtocolVersion version) { - switch (version) { - case V1: - case V2: - return getUnsignedShort(input); - case V3: - case V4: - case V5: - case V6: - return input.getInt(); - default: - throw version.unsupported(); - } - } - - /** - * Utility method that writes a size value. Mainly intended for collection codecs when serializing - * CQL collections. - * - * @param output The ByteBuffer to write to. - * @param size The collection size. - * @param version The protocol version to use. - */ - public static void writeSize(ByteBuffer output, int size, ProtocolVersion version) { - switch (version) { - case V1: - case V2: - if (size > 65535) - throw new IllegalArgumentException( - String.format( - "Native protocol version %d supports up to 65535 elements in any collection - but collection contains %d elements", - version.toInt(), size)); - output.putShort((short) size); - break; - case V3: - case V4: - case V5: - case V6: - output.putInt(size); - break; - default: - throw version.unsupported(); - } - } - - /** - * Utility method that reads a value. Mainly intended for collection codecs when deserializing CQL - * collections. - * - * @param input The ByteBuffer to read from. - * @param version The protocol version to use. - * @return The collection element. - */ - public static ByteBuffer readValue(ByteBuffer input, ProtocolVersion version) { - int size = readSize(input, version); - return size < 0 ? null : readBytes(input, size); - } - - /** - * Utility method that writes a value. Mainly intended for collection codecs when deserializing - * CQL collections. - * - * @param output The ByteBuffer to write to. - * @param value The value to write. - * @param version The protocol version to use. - */ - public static void writeValue(ByteBuffer output, ByteBuffer value, ProtocolVersion version) { - switch (version) { - case V1: - case V2: - assert value != null; - output.putShort((short) value.remaining()); - output.put(value.duplicate()); - break; - case V3: - case V4: - case V5: - case V6: - if (value == null) { - output.putInt(-1); - } else { - output.putInt(value.remaining()); - output.put(value.duplicate()); - } - break; - default: - throw version.unsupported(); - } - } - - /** - * Read {@code length} bytes from {@code bb} into a new ByteBuffer. - * - * @param bb The ByteBuffer to read. - * @param length The number of bytes to read. - * @return The read bytes. - */ - public static ByteBuffer readBytes(ByteBuffer bb, int length) { - ByteBuffer copy = bb.duplicate(); - copy.limit(copy.position() + length); - bb.position(bb.position() + length); - return copy; - } - - /** - * Converts an "unsigned" int read from a DATE value into a signed int. - * - *

The protocol encodes DATE values as unsigned ints with the Epoch in the middle of - * the range (2^31). This method handles the conversion from an "unsigned" to a signed int. - */ - public static int fromUnsignedToSignedInt(int unsigned) { - return unsigned + Integer.MIN_VALUE; // this relies on overflow for "negative" values - } - - /** - * Converts an int into an "unsigned" int suitable to be written as a DATE value. - * - *

The protocol encodes DATE values as unsigned ints with the Epoch in the middle of - * the range (2^31). This method handles the conversion from a signed to an "unsigned" int. - */ - public static int fromSignedToUnsignedInt(int signed) { - return signed - Integer.MIN_VALUE; - } - - /** - * Convert from a raw CQL long representing a numeric DATE literal to the number of days since the - * Epoch. In CQL, numeric DATE literals are longs (unsigned integers actually) between 0 and 2^32 - * - 1, with the epoch in the middle; this method re-centers the epoch at 0. - * - * @param raw The CQL date value to convert. - * @return The number of days since the Epoch corresponding to the given raw value. - * @throws IllegalArgumentException if the value is out of range. - */ - public static int fromCqlDateToDaysSinceEpoch(long raw) { - if (raw < 0 || raw > MAX_CQL_LONG_VALUE) - throw new IllegalArgumentException( - String.format( - "Numeric literals for DATE must be between 0 and %d (got %d)", - MAX_CQL_LONG_VALUE, raw)); - return (int) (raw - EPOCH_AS_CQL_LONG); - } - - /** - * Convert the number of days since the Epoch into a raw CQL long representing a numeric DATE - * literal. - * - *

In CQL, numeric DATE literals are longs (unsigned integers actually) between 0 and 2^32 - 1, - * with the epoch in the middle; this method re-centers the epoch at 2^31. - * - * @param days The number of days since the Epoch convert. - * @return The CQL date value corresponding to the given value. - */ - public static long fromDaysSinceEpochToCqlDate(int days) { - return ((long) days + EPOCH_AS_CQL_LONG); - } - - private static int sizeOfCollectionSize(ProtocolVersion version) { - switch (version) { - case V1: - case V2: - return 2; - case V3: - case V4: - case V5: - case V6: - return 4; - default: - throw version.unsupported(); - } - } - - private static int sizeOfValue(ByteBuffer value, ProtocolVersion version) { - switch (version) { - case V1: - case V2: - int elemSize = value.remaining(); - if (elemSize > 65535) - throw new IllegalArgumentException( - String.format( - "Native protocol version %d supports only elements with size up to 65535 bytes - but element size is %d bytes", - version.toInt(), elemSize)); - return 2 + elemSize; - case V3: - case V4: - case V5: - case V6: - return value == null ? 4 : 4 + value.remaining(); - default: - throw version.unsupported(); - } - } - - private static int getUnsignedShort(ByteBuffer bb) { - int length = (bb.get() & 0xFF) << 8; - return length | (bb.get() & 0xFF); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ColumnDefinitions.java b/driver-core/src/main/java/com/datastax/driver/core/ColumnDefinitions.java deleted file mode 100644 index e2ed07fe16c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ColumnDefinitions.java +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * Metadata describing the columns returned in a {@link ResultSet} or a {@link PreparedStatement}. - * - *

A {@code columnDefinitions}} instance is mainly a list of {@code - * ColumnsDefinitions.Definition}. The definitions or metadata for a column can be accessed either - * by: - * - *

- * - *

When accessed by name, column selection is case insensitive. In case multiple columns only - * differ by the case of their name, then the column returned with be the first column that has been - * defined in CQL without forcing case sensitivity (that is, it has either been defined without - * quotes or is fully lowercase). If none of the columns have been defined in this manner, the first - * column matching (with case insensitivity) is returned. You can force the case of a selection by - * double quoting the name. - * - *

For example: - * - *

    - *
  • If {@code cd} contains column {@code fOO}, then {@code cd.contains("foo")}, {@code - * cd.contains("fOO")} and {@code cd.contains("Foo")} will return {@code true}. - *
  • If {@code cd} contains both {@code foo} and {@code FOO} then: - *
      - *
    • {@code cd.getType("foo")}, {@code cd.getType("fOO")} and {@code cd.getType("FOO")} - * will all match column {@code foo}. - *
    • {@code cd.getType("\"FOO\"")} will match column {@code FOO} - *
    - *
- * - * Note that the preceding rules mean that if a {@code ColumnDefinitions} object contains multiple - * occurrences of the exact same name (be it the same column multiple times or columns from - * different tables with the same name), you will have to use selection by index to disambiguate. - */ -public class ColumnDefinitions implements Iterable { - - static final ColumnDefinitions EMPTY = - new ColumnDefinitions(new Definition[0], CodecRegistry.DEFAULT_INSTANCE); - - private final Definition[] byIdx; - private final Map byName; - final CodecRegistry codecRegistry; - - ColumnDefinitions(Definition[] defs, CodecRegistry codecRegistry) { - - this.byIdx = defs; - this.codecRegistry = codecRegistry; - this.byName = new HashMap(defs.length); - - for (int i = 0; i < defs.length; i++) { - // Be optimistic, 99% of the time, previous will be null. - int[] previous = this.byName.put(defs[i].name.toLowerCase(), new int[] {i}); - if (previous != null) { - int[] indexes = new int[previous.length + 1]; - System.arraycopy(previous, 0, indexes, 0, previous.length); - indexes[indexes.length - 1] = i; - this.byName.put(defs[i].name.toLowerCase(), indexes); - } - } - } - - /** - * Returns the number of columns described by this {@code Columns} instance. - * - * @return the number of columns described by this metadata. - */ - public int size() { - return byIdx.length; - } - - /** - * Returns whether this metadata contains a given name. - * - * @param name the name to check. - * @return {@code true} if this metadata contains the column named {@code name}, {@code false} - * otherwise. - */ - public boolean contains(String name) { - return findAllIdx(name) != null; - } - - /** - * The first index in this metadata of the provided name, if present. - * - * @param name the name of the column. - * @return the index of the first occurrence of {@code name} in this metadata if {@code - * contains(name)}, -1 otherwise. - */ - public int getIndexOf(String name) { - return findFirstIdx(name); - } - - /** - * Returns an iterator over the {@link Definition} contained in this metadata. - * - *

The order of the iterator will be the one of this metadata. - * - * @return an iterator over the {@link Definition} contained in this metadata. - */ - @Override - public Iterator iterator() { - return Arrays.asList(byIdx).iterator(); - } - - /** - * Returns a list containing all the definitions of this metadata in order. - * - * @return a list of the {@link Definition} contained in this metadata. - */ - public List asList() { - return Arrays.asList(byIdx); - } - - /** - * Returns the name of the {@code i}th column in this metadata. - * - * @param i the index in this metadata. - * @return the name of the {@code i}th column in this metadata. - * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i >= size()} - */ - public String getName(int i) { - return byIdx[i].name; - } - - /** - * Returns the type of the {@code i}th column in this metadata. - * - *

Note that this method does not set the {@link DataType#isFrozen()} flag on the returned - * object, it will always default to {@code false}. Use {@link Cluster#getMetadata()} to determine - * if a column is frozen. - * - * @param i the index in this metadata. - * @return the type of the {@code i}th column in this metadata. - * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i >= size()} - */ - public DataType getType(int i) { - return byIdx[i].type; - } - - /** - * Returns the type of the first occurrence of {@code name} in this metadata. - * - *

Note that this method does not set the {@link DataType#isFrozen()} flag on the returned - * object, it will always default to {@code false}. Use {@link Cluster#getMetadata()} to determine - * if a column is frozen. - * - * @param name the name of the column. - * @return the type of (the first occurrence of) {@code name} in this metadata. - * @throws IllegalArgumentException if {@code name} is not in this metadata. - */ - public DataType getType(String name) { - return getType(getFirstIdx(name)); - } - - /** - * Returns the keyspace of the {@code i}th column in this metadata. - * - * @param i the index in this metadata. - * @return the keyspace of the {@code i}th column in this metadata. - * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i >= size()} - */ - public String getKeyspace(int i) { - return byIdx[i].keyspace; - } - - /** - * Returns the keyspace of the first occurrence of {@code name} in this metadata. - * - * @param name the name of the column. - * @return the keyspace of (the first occurrence of) column {@code name} in this metadata. - * @throws IllegalArgumentException if {@code name} is not in this metadata. - */ - public String getKeyspace(String name) { - return getKeyspace(getFirstIdx(name)); - } - - /** - * Returns the table of the {@code i}th column in this metadata. - * - * @param i the index in this metadata. - * @return the table of the {@code i}th column in this metadata. - * @throws IndexOutOfBoundsException if {@code i < 0} or {@code i >= size()} - */ - public String getTable(int i) { - return byIdx[i].table; - } - - /** - * Returns the table of first occurrence of {@code name} in this metadata. - * - * @param name the name of the column. - * @return the table of (the first occurrence of) column {@code name} in this metadata. - * @throws IllegalArgumentException if {@code name} is not in this metadata. - */ - public String getTable(String name) { - return getTable(getFirstIdx(name)); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("Columns["); - for (int i = 0; i < size(); i++) { - if (i != 0) sb.append(", "); - Definition def = byIdx[i]; - sb.append(def.name).append('(').append(def.type).append(')'); - } - sb.append(']'); - return sb.toString(); - } - - int findFirstIdx(String name) { - - int[] indexes = findAllIdx(name); - return indexes == null ? -1 : indexes[0]; - } - - int[] findAllIdx(String name) { - boolean caseSensitive = false; - if (name.length() >= 2 && name.charAt(0) == '"' && name.charAt(name.length() - 1) == '"') { - name = name.substring(1, name.length() - 1); - caseSensitive = true; - } - - int[] indexes = byName.get(name.toLowerCase()); - if (!caseSensitive || indexes == null) return indexes; - - // First, optimistic and assume all are matching - int nbMatch = 0; - for (int i = 0; i < indexes.length; i++) if (name.equals(byIdx[indexes[i]].name)) nbMatch++; - - if (nbMatch == indexes.length) return indexes; - - int[] result = new int[nbMatch]; - int j = 0; - for (int i = 0; i < indexes.length; i++) { - int idx = indexes[i]; - if (name.equals(byIdx[idx].name)) result[j++] = idx; - } - - return result; - } - - int[] getAllIdx(String name) { - int[] indexes = findAllIdx(name); - if (indexes == null) - throw new IllegalArgumentException(name + " is not a column defined in this metadata"); - - return indexes; - } - - int getFirstIdx(String name) { - return getAllIdx(name)[0]; - } - - /** A column definition. */ - public static class Definition { - - private final String keyspace; - private final String table; - private final String name; - private final DataType type; - - Definition(String keyspace, String table, String name, DataType type) { - this.keyspace = keyspace; - this.table = table; - this.name = name; - this.type = type; - } - - /** - * The name of the keyspace this column is part of. - * - * @return the name of the keyspace this column is part of. - */ - public String getKeyspace() { - return keyspace; - } - - /** - * Returns the name of the table this column is part of. - * - * @return the name of the table this column is part of. - */ - public String getTable() { - return table; - } - - /** - * Returns the name of the column. - * - * @return the name of the column. - */ - public String getName() { - return name; - } - - /** - * Returns the type of the column. - * - * @return the type of the column. - */ - public DataType getType() { - return type; - } - - @Override - public final int hashCode() { - return Arrays.hashCode(new Object[] {keyspace, table, name, type}); - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof Definition)) return false; - - Definition other = (Definition) o; - return keyspace.equals(other.keyspace) - && table.equals(other.table) - && name.equals(other.name) - && type.equals(other.type); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ColumnMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/ColumnMetadata.java deleted file mode 100644 index cd27231010c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ColumnMetadata.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.MoreObjects; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/** Describes a Column. */ -public class ColumnMetadata { - - static final String COLUMN_NAME = "column_name"; - - static final String VALIDATOR = "validator"; // v2 only - static final String TYPE = "type"; // replaces validator, v3 onwards - - static final String COMPONENT_INDEX = "component_index"; // v2 only - static final String POSITION = "position"; // replaces component_index, v3 onwards - - static final String KIND_V2 = "type"; // v2 only - static final String KIND_V3 = "kind"; // replaces type, v3 onwards - - static final String CLUSTERING_ORDER = "clustering_order"; - static final String DESC = "desc"; - - static final String INDEX_TYPE = "index_type"; - static final String INDEX_OPTIONS = "index_options"; - static final String INDEX_NAME = "index_name"; - - private final AbstractTableMetadata parent; - private final String name; - private final DataType type; - private final boolean isStatic; - - private ColumnMetadata( - AbstractTableMetadata parent, String name, DataType type, boolean isStatic) { - this.parent = parent; - this.name = name; - this.type = type; - this.isStatic = isStatic; - } - - static ColumnMetadata fromRaw(AbstractTableMetadata tm, Raw raw, DataType dataType) { - return new ColumnMetadata(tm, raw.name, dataType, raw.kind == Raw.Kind.STATIC); - } - - static ColumnMetadata forAlias(TableMetadata tm, String name, DataType type) { - return new ColumnMetadata(tm, name, type, false); - } - - /** - * Returns the name of the column. - * - * @return the name of the column. - */ - public String getName() { - return name; - } - - /** - * Returns the parent object of this column. This can be a {@link TableMetadata} or a {@link - * MaterializedViewMetadata} object. - * - * @return the parent object of this column. - */ - public AbstractTableMetadata getParent() { - return parent; - } - - /** - * Returns the type of the column. - * - * @return the type of the column. - */ - public DataType getType() { - return type; - } - - /** - * Whether this column is a static column. - * - * @return Whether this column is a static column or not. - */ - public boolean isStatic() { - return isStatic; - } - - @Override - public boolean equals(Object other) { - if (other == this) return true; - if (!(other instanceof ColumnMetadata)) return false; - - ColumnMetadata that = (ColumnMetadata) other; - return this.name.equals(that.name) - && this.isStatic == that.isStatic - && this.type.equals(that.type); - } - - @Override - public int hashCode() { - return MoreObjects.hashCode(name, isStatic, type); - } - - @Override - public String toString() { - String str = Metadata.quoteIfNecessary(name) + ' ' + type; - return isStatic ? str + " static" : str; - } - - // Temporary class that is used to make building the schema easier. Not meant to be - // exposed publicly at all. - static class Raw { - - public enum Kind { - PARTITION_KEY("PARTITION_KEY", "PARTITION_KEY"), - CLUSTERING_COLUMN("CLUSTERING_KEY", "CLUSTERING"), - REGULAR("REGULAR", "REGULAR"), - COMPACT_VALUE("COMPACT_VALUE", ""), // v2 only - STATIC("STATIC", "STATIC"); - - final String v2; - final String v3; - - Kind(String v2, String v3) { - this.v2 = v2; - this.v3 = v3; - } - - static Kind fromStringV2(String s) { - for (Kind kind : Kind.values()) { - if (kind.v2.equalsIgnoreCase(s)) return kind; - } - throw new IllegalArgumentException(s); - } - - static Kind fromStringV3(String s) { - for (Kind kind : Kind.values()) { - if (kind.v3.equalsIgnoreCase(s)) return kind; - } - throw new IllegalArgumentException(s); - } - } - - public final String name; - public Kind kind; - public final int position; - public final String dataType; - public final boolean isReversed; - - public final Map indexColumns = new HashMap(); - - Raw(String name, Kind kind, int position, String dataType, boolean isReversed) { - this.name = name; - this.kind = kind; - this.position = position; - this.dataType = dataType; - this.isReversed = isReversed; - } - - static Raw fromRow(Row row, VersionNumber version) { - String name = row.getString(COLUMN_NAME); - - Kind kind; - if (version.getMajor() < 2) { - kind = Kind.REGULAR; - } else if (version.getMajor() < 3) { - kind = row.isNull(KIND_V2) ? Kind.REGULAR : Kind.fromStringV2(row.getString(KIND_V2)); - } else { - kind = row.isNull(KIND_V3) ? Kind.REGULAR : Kind.fromStringV3(row.getString(KIND_V3)); - } - - int position; - if (version.getMajor() >= 3) { - position = - row.getInt( - POSITION); // cannot be null, -1 is used as a special value instead of null to avoid - // tombstones - if (position == -1) position = 0; - } else { - position = row.isNull(COMPONENT_INDEX) ? 0 : row.getInt(COMPONENT_INDEX); - } - - String dataType; - boolean reversed; - if (version.getMajor() >= 3) { - dataType = row.getString(TYPE); - String clusteringOrderStr = row.getString(CLUSTERING_ORDER); - reversed = clusteringOrderStr.equals(DESC); - } else { - dataType = row.getString(VALIDATOR); - reversed = DataTypeClassNameParser.isReversed(dataType); - } - - Raw c = new Raw(name, kind, position, dataType, reversed); - - // secondary indexes (C* < 3.0.0) - // from C* 3.0 onwards 2i are defined in a separate table - if (version.getMajor() < 3) { - for (String str : Arrays.asList(INDEX_TYPE, INDEX_NAME, INDEX_OPTIONS)) - if (row.getColumnDefinitions().contains(str) && !row.isNull(str)) - c.indexColumns.put(str, row.getString(str)); - } - return c; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Configuration.java b/driver-core/src/main/java/com/datastax/driver/core/Configuration.java deleted file mode 100644 index 3ef6922df1b..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Configuration.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.policies.Policies; -import com.google.common.base.Joiner; -import java.util.ArrayList; -import java.util.List; - -/** - * The configuration of the cluster. It configures the following: - * - *

    - *
  • Cassandra protocol level configuration (compression). - *
  • Connection pooling configurations. - *
  • low-level TCP configuration options (tcpNoDelay, keepAlive, ...). - *
  • Metrics related options. - *
  • Query related options (default consistency level, fetchSize, ...). - *
  • Netty layer customization options. - *
- * - * This is also where you get the configured policies, though those cannot be changed (they are set - * during the built of the Cluster object). - */ -public class Configuration { - - /** - * Returns a builder to create a new {@code Configuration} object. - * - *

You only need this if you are building the configuration yourself. If you use {@link - * Cluster#builder()}, it will be done under the hood for you. - * - * @return the builder. - */ - public static Builder builder() { - return new Builder(); - } - - private final Policies policies; - private final ProtocolOptions protocolOptions; - private final PoolingOptions poolingOptions; - private final SocketOptions socketOptions; - private final MetricsOptions metricsOptions; - private final QueryOptions queryOptions; - private final ThreadingOptions threadingOptions; - private final NettyOptions nettyOptions; - private final CodecRegistry codecRegistry; - private final String defaultKeyspace; - - private Configuration( - Policies policies, - ProtocolOptions protocolOptions, - PoolingOptions poolingOptions, - SocketOptions socketOptions, - MetricsOptions metricsOptions, - QueryOptions queryOptions, - ThreadingOptions threadingOptions, - NettyOptions nettyOptions, - CodecRegistry codecRegistry, - String defaultKeyspace) { - this.policies = policies; - this.protocolOptions = protocolOptions; - this.poolingOptions = poolingOptions; - this.socketOptions = socketOptions; - this.metricsOptions = metricsOptions; - this.queryOptions = queryOptions; - this.threadingOptions = threadingOptions; - this.nettyOptions = nettyOptions; - this.codecRegistry = codecRegistry; - this.defaultKeyspace = defaultKeyspace; - } - - /** - * Copy constructor. - * - * @param toCopy the object to copy from. - */ - protected Configuration(Configuration toCopy) { - this( - toCopy.getPolicies(), - toCopy.getProtocolOptions(), - toCopy.getPoolingOptions(), - toCopy.getSocketOptions(), - toCopy.getMetricsOptions(), - toCopy.getQueryOptions(), - toCopy.getThreadingOptions(), - toCopy.getNettyOptions(), - toCopy.getCodecRegistry(), - toCopy.getDefaultKeyspace()); - } - - void register(Cluster.Manager manager) { - protocolOptions.register(manager); - poolingOptions.register(manager); - queryOptions.register(manager); - policies.getEndPointFactory().init(manager.getCluster()); - - checkPoliciesIfSni(); - } - - // If using SNI endpoints, the SSL options and auth provider MUST be the "extended" versions, the - // base versions work with IP addresses that might not be unique to a node. - // Throw now since that's probably a configuration error. - private void checkPoliciesIfSni() { - if (policies.getEndPointFactory() instanceof SniEndPointFactory) { - SSLOptions sslOptions = protocolOptions.getSSLOptions(); - List errors = new ArrayList(); - if (sslOptions != null && !(sslOptions instanceof ExtendedRemoteEndpointAwareSslOptions)) { - errors.add( - String.format( - "the configured %s must implement %s", - SSLOptions.class.getSimpleName(), - ExtendedRemoteEndpointAwareSslOptions.class.getSimpleName())); - } - AuthProvider authProvider = protocolOptions.getAuthProvider(); - if (authProvider != null && !(authProvider instanceof ExtendedAuthProvider)) { - errors.add( - String.format( - "the configured %s must implement %s", - AuthProvider.class.getSimpleName(), ExtendedAuthProvider.class.getSimpleName())); - } - if (!errors.isEmpty()) { - throw new IllegalStateException( - "Configuration error: if SNI endpoints are in use, " + Joiner.on(',').join(errors)); - } - } - } - - /** - * Returns the policies set for the cluster. - * - * @return the policies set for the cluster. - */ - public Policies getPolicies() { - return policies; - } - - /** - * Returns the low-level TCP configuration options used (tcpNoDelay, keepAlive, ...). - * - * @return the socket options. - */ - public SocketOptions getSocketOptions() { - return socketOptions; - } - - /** - * Returns the Cassandra binary protocol level configuration (compression). - * - * @return the protocol options. - */ - public ProtocolOptions getProtocolOptions() { - return protocolOptions; - } - - /** - * Returns the connection pooling configuration. - * - * @return the pooling options. - */ - public PoolingOptions getPoolingOptions() { - return poolingOptions; - } - - /** - * Returns the metrics configuration, if metrics are enabled. - * - *

Metrics collection is enabled by default but can be disabled at cluster construction time - * through {@link Cluster.Builder#withoutMetrics}. - * - * @return the metrics options or {@code null} if metrics are not enabled. - */ - public MetricsOptions getMetricsOptions() { - return metricsOptions; - } - - /** - * Returns the queries configuration. - * - * @return the queries options. - */ - public QueryOptions getQueryOptions() { - return queryOptions; - } - - /** @return the threading options for this configuration. */ - public ThreadingOptions getThreadingOptions() { - return threadingOptions; - } - - /** - * Returns the {@link NettyOptions} instance for this configuration. - * - * @return the {@link NettyOptions} instance for this configuration. - */ - public NettyOptions getNettyOptions() { - return nettyOptions; - } - - public String getDefaultKeyspace() { - return defaultKeyspace; - } - /** - * Returns the {@link CodecRegistry} instance for this configuration. - * - *

Note that this method could return {@link CodecRegistry#DEFAULT_INSTANCE} if no specific - * codec registry has been set on the {@link Cluster}. In this case, care should be taken when - * registering new codecs as they would be immediately available to other {@link Cluster} - * instances sharing the same default instance. - * - * @return the {@link CodecRegistry} instance for this configuration. - */ - public CodecRegistry getCodecRegistry() { - return codecRegistry; - } - - /** A builder to create a new {@code Configuration} object. */ - public static class Builder { - private Policies policies; - private ProtocolOptions protocolOptions; - private PoolingOptions poolingOptions; - private SocketOptions socketOptions; - private MetricsOptions metricsOptions; - private QueryOptions queryOptions; - private ThreadingOptions threadingOptions; - private NettyOptions nettyOptions; - private CodecRegistry codecRegistry; - private String defaultKeyspace; - - /** - * Sets the policies for this cluster. - * - * @param policies the policies. - * @return this builder. - */ - public Builder withPolicies(Policies policies) { - this.policies = policies; - return this; - } - - /** - * Sets the protocol options for this cluster. - * - * @param protocolOptions the protocol options. - * @return this builder. - */ - public Builder withProtocolOptions(ProtocolOptions protocolOptions) { - this.protocolOptions = protocolOptions; - return this; - } - - /** - * Sets the pooling options for this cluster. - * - * @param poolingOptions the pooling options. - * @return this builder. - */ - public Builder withPoolingOptions(PoolingOptions poolingOptions) { - this.poolingOptions = poolingOptions; - return this; - } - - /** - * Sets the socket options for this cluster. - * - * @param socketOptions the socket options. - * @return this builder. - */ - public Builder withSocketOptions(SocketOptions socketOptions) { - this.socketOptions = socketOptions; - return this; - } - - /** - * Sets the metrics options for this cluster. - * - *

If this method doesn't get called, the configuration will use the defaults: metrics - * enabled with JMX reporting enabled. To disable metrics, call this method with an instance - * where {@link MetricsOptions#isEnabled() isEnabled()} returns false. - * - * @param metricsOptions the metrics options. - * @return this builder. - */ - public Builder withMetricsOptions(MetricsOptions metricsOptions) { - this.metricsOptions = metricsOptions; - return this; - } - - /** - * Sets the query options for this cluster. - * - * @param queryOptions the query options. - * @return this builder. - */ - public Builder withQueryOptions(QueryOptions queryOptions) { - this.queryOptions = queryOptions; - return this; - } - - /** - * Sets the threading options for this cluster. - * - * @param threadingOptions the threading options to set. - * @return this builder. - */ - public Builder withThreadingOptions(ThreadingOptions threadingOptions) { - this.threadingOptions = threadingOptions; - return this; - } - - /** - * Sets the Netty options for this cluster. - * - * @param nettyOptions the Netty options. - * @return this builder. - */ - public Builder withNettyOptions(NettyOptions nettyOptions) { - this.nettyOptions = nettyOptions; - return this; - } - - /** - * Sets the codec registry for this cluster. - * - * @param codecRegistry the codec registry. - * @return this builder. - */ - public Builder withCodecRegistry(CodecRegistry codecRegistry) { - this.codecRegistry = codecRegistry; - return this; - } - - public Builder withDefaultKeyspace(String keyspace) { - this.defaultKeyspace = keyspace; - return this; - } - - /** - * Builds the final object from this builder. - * - *

Any field that hasn't been set explicitly will get its default value. - * - * @return the object. - */ - public Configuration build() { - return new Configuration( - policies != null ? policies : Policies.builder().build(), - protocolOptions != null ? protocolOptions : new ProtocolOptions(), - poolingOptions != null ? poolingOptions : new PoolingOptions(), - socketOptions != null ? socketOptions : new SocketOptions(), - metricsOptions != null ? metricsOptions : new MetricsOptions(), - queryOptions != null ? queryOptions : new QueryOptions(), - threadingOptions != null ? threadingOptions : new ThreadingOptions(), - nettyOptions != null ? nettyOptions : NettyOptions.DEFAULT_INSTANCE, - codecRegistry != null ? codecRegistry : CodecRegistry.DEFAULT_INSTANCE, - defaultKeyspace); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Connection.java b/driver-core/src/main/java/com/datastax/driver/core/Connection.java deleted file mode 100644 index 26ee6c91b0a..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Connection.java +++ /dev/null @@ -1,1773 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.Message.Response.Type.ERROR; -import static io.netty.handler.timeout.IdleState.READER_IDLE; - -import com.datastax.driver.core.Responses.Result.SetKeyspace; -import com.datastax.driver.core.exceptions.AuthenticationException; -import com.datastax.driver.core.exceptions.BusyConnectionException; -import com.datastax.driver.core.exceptions.ConnectionException; -import com.datastax.driver.core.exceptions.CrcMismatchException; -import com.datastax.driver.core.exceptions.DriverException; -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.exceptions.FrameTooLongException; -import com.datastax.driver.core.exceptions.OperationTimedOutException; -import com.datastax.driver.core.exceptions.TransportException; -import com.datastax.driver.core.exceptions.UnsupportedProtocolVersionException; -import com.datastax.driver.core.utils.MoreFutures; -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Lists; -import com.google.common.collect.MapMaker; -import com.google.common.util.concurrent.AbstractFuture; -import com.google.common.util.concurrent.AsyncFunction; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.SettableFuture; -import com.google.common.util.concurrent.Uninterruptibles; -import io.netty.bootstrap.Bootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelOption; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.EventLoop; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.channel.group.ChannelGroup; -import io.netty.channel.group.DefaultChannelGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.DecoderException; -import io.netty.handler.ssl.SslHandler; -import io.netty.handler.timeout.IdleStateEvent; -import io.netty.handler.timeout.IdleStateHandler; -import io.netty.util.Timeout; -import io.netty.util.Timer; -import io.netty.util.TimerTask; -import io.netty.util.concurrent.GlobalEventExecutor; -import java.lang.ref.WeakReference; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -// For LoggingHandler -// import org.jboss.netty.handler.logging.LoggingHandler; -// import org.jboss.netty.logging.InternalLogLevel; - -/** A connection to a Cassandra Node. */ -class Connection { - - private static final Logger logger = LoggerFactory.getLogger(Connection.class); - private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - - private static final boolean DISABLE_COALESCING = - SystemProperties.getBoolean("com.datastax.driver.DISABLE_COALESCING", false); - private static final int FLUSHER_SCHEDULE_PERIOD_NS = - SystemProperties.getInt("com.datastax.driver.FLUSHER_SCHEDULE_PERIOD_NS", 10000); - - enum State { - OPEN, - TRASHED, - RESURRECTING, - GONE - } - - final AtomicReference state = new AtomicReference(State.OPEN); - - volatile long maxIdleTime; - - final EndPoint endPoint; - private final String name; - - @VisibleForTesting volatile Channel channel; - private final Factory factory; - - @VisibleForTesting final Dispatcher dispatcher; - - // Used by connection pooling to count how many requests are "in flight" on that connection. - final AtomicInteger inFlight = new AtomicInteger(0); - - private final AtomicInteger writer = new AtomicInteger(0); - - private final AtomicReference targetKeyspace; - private final SetKeyspaceAttempt defaultKeyspaceAttempt; - - private volatile boolean isInitialized; - private final AtomicBoolean isDefunct = new AtomicBoolean(); - private final AtomicBoolean signaled = new AtomicBoolean(); - - private final AtomicReference closeFuture = - new AtomicReference(); - - private final AtomicReference ownerRef = new AtomicReference(); - - /** - * Create a new connection to a Cassandra node and associate it with the given pool. - * - * @param name the connection name - * @param endPoint the information to connect to the node - * @param factory the connection factory to use - * @param owner the component owning this connection (may be null). Note that an existing - * connection can also be associated to an owner later with {@link #setOwner(Owner)}. - */ - protected Connection(String name, EndPoint endPoint, Factory factory, Owner owner) { - this.endPoint = endPoint; - this.factory = factory; - this.dispatcher = new Dispatcher(); - this.name = name; - this.ownerRef.set(owner); - ListenableFuture thisFuture = Futures.immediateFuture(this); - this.defaultKeyspaceAttempt = new SetKeyspaceAttempt(null, thisFuture); - this.targetKeyspace = new AtomicReference(defaultKeyspaceAttempt); - } - - /** Create a new connection to a Cassandra node. */ - Connection(String name, EndPoint endPoint, Factory factory) { - this(name, endPoint, factory, null); - } - - ListenableFuture initAsync() { - if (factory.isShutdown) - return Futures.immediateFailedFuture( - new ConnectionException(endPoint, "Connection factory is shut down")); - - ProtocolVersion protocolVersion = - factory.protocolVersion == null - ? ProtocolVersion.NEWEST_SUPPORTED - : factory.protocolVersion; - final SettableFuture channelReadyFuture = SettableFuture.create(); - - try { - Bootstrap bootstrap = factory.newBootstrap(); - ProtocolOptions protocolOptions = factory.configuration.getProtocolOptions(); - bootstrap.handler( - new Initializer( - this, - protocolVersion, - protocolOptions.getCompression().compressor(), - protocolOptions.getSSLOptions(), - factory.configuration.getPoolingOptions().getHeartbeatIntervalSeconds(), - factory.configuration.getNettyOptions(), - factory.configuration.getCodecRegistry(), - factory.configuration.getMetricsOptions().isEnabled() - ? factory.manager.metrics - : null)); - - ChannelFuture future = bootstrap.connect(endPoint.resolve()); - - writer.incrementAndGet(); - future.addListener( - new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - writer.decrementAndGet(); - // Note: future.channel() can be null in some error cases, so we need to guard against - // it in the rest of the code below. - channel = future.channel(); - if (isClosed() && channel != null) { - channel - .close() - .addListener( - new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - channelReadyFuture.setException( - new TransportException( - Connection.this.endPoint, - "Connection closed during initialization.")); - } - }); - } else { - if (channel != null) { - Connection.this.factory.allChannels.add(channel); - } - if (!future.isSuccess()) { - if (logger.isDebugEnabled()) - logger.debug( - String.format( - "%s Error connecting to %s%s", - Connection.this, - Connection.this.endPoint, - extractMessage(future.cause()))); - channelReadyFuture.setException( - new TransportException( - Connection.this.endPoint, "Cannot connect", future.cause())); - } else { - assert channel != null; - logger.debug( - "{} Connection established, initializing transport", Connection.this); - channel.closeFuture().addListener(new ChannelCloseListener()); - channelReadyFuture.set(null); - } - } - } - }); - } catch (RuntimeException e) { - closeAsync().force(); - throw e; - } - - Executor initExecutor = - factory.manager.configuration.getPoolingOptions().getInitializationExecutor(); - - ListenableFuture initializeTransportFuture = - GuavaCompatibility.INSTANCE.transformAsync( - channelReadyFuture, onChannelReady(protocolVersion, initExecutor), initExecutor); - - // Fallback on initializeTransportFuture so we can properly propagate specific exceptions. - ListenableFuture initFuture = - GuavaCompatibility.INSTANCE.withFallback( - initializeTransportFuture, - new AsyncFunction() { - @Override - public ListenableFuture apply(Throwable t) throws Exception { - SettableFuture future = SettableFuture.create(); - // Make sure the connection gets properly closed. - if (t instanceof ClusterNameMismatchException - || t instanceof UnsupportedProtocolVersionException) { - // Just propagate - closeAsync().force(); - future.setException(t); - } else { - // Defunct to ensure that the error will be signaled (marking the host down) - Throwable e = - (t instanceof ConnectionException - || t instanceof DriverException - || t instanceof InterruptedException - || t instanceof Error) - ? t - : new ConnectionException( - Connection.this.endPoint, - String.format( - "Unexpected error during transport initialization (%s)", t), - t); - future.setException(defunct(e)); - } - return future; - } - }, - initExecutor); - - // Ensure the connection gets closed if the caller cancels the returned future. - GuavaCompatibility.INSTANCE.addCallback( - initFuture, - new MoreFutures.FailureCallback() { - @Override - public void onFailure(Throwable t) { - if (!isClosed()) { - closeAsync().force(); - } - } - }, - initExecutor); - - return initFuture; - } - - private static String extractMessage(Throwable t) { - if (t == null) return ""; - String msg = t.getMessage() == null || t.getMessage().isEmpty() ? t.toString() : t.getMessage(); - return " (" + msg + ')'; - } - - public ListenableFuture optionsQuery() { - Future startupOptionsFuture = write(new Requests.Options()); - - return GuavaCompatibility.INSTANCE.transformAsync(startupOptionsFuture, onSupportedResponse()); - } - - private AsyncFunction onChannelReady( - final ProtocolVersion protocolVersion, final Executor initExecutor) { - return new AsyncFunction() { - @Override - public ListenableFuture apply(Void input) throws Exception { - ProtocolOptions protocolOptions = factory.configuration.getProtocolOptions(); - Future startupResponseFuture = - write( - new Requests.Startup( - protocolOptions.getCompression(), protocolOptions.isNoCompact())); - return GuavaCompatibility.INSTANCE.transformAsync( - startupResponseFuture, onStartupResponse(protocolVersion, initExecutor), initExecutor); - } - }; - } - - private AsyncFunction onSupportedResponse() { - return new AsyncFunction() { - @Override - public ListenableFuture apply(Message.Response response) throws Exception { - switch (response.type) { - case SUPPORTED: - return getProductType((Responses.Supported) response); - case ERROR: - Responses.Error error = (Responses.Error) response; - throw new TransportException( - endPoint, String.format("Error initializing connection: %s", error.message)); - default: - throw new TransportException( - endPoint, - String.format( - "Unexpected %s response message from server to a STARTUP message", - response.type)); - } - } - }; - } - - private AsyncFunction onStartupResponse( - final ProtocolVersion protocolVersion, final Executor initExecutor) { - return new AsyncFunction() { - @Override - public ListenableFuture apply(Message.Response response) throws Exception { - switch (response.type) { - case READY: - return checkClusterName(protocolVersion, initExecutor); - case ERROR: - Responses.Error error = (Responses.Error) response; - if (isUnsupportedProtocolVersion(error)) - throw unsupportedProtocolVersionException( - protocolVersion, error.serverProtocolVersion); - throw new TransportException( - endPoint, String.format("Error initializing connection: %s", error.message)); - case AUTHENTICATE: - Responses.Authenticate authenticate = (Responses.Authenticate) response; - Authenticator authenticator; - try { - if (factory.authProvider instanceof ExtendedAuthProvider) { - authenticator = - ((ExtendedAuthProvider) factory.authProvider) - .newAuthenticator(endPoint, authenticate.authenticator); - } else { - authenticator = - factory.authProvider.newAuthenticator( - endPoint.resolve(), authenticate.authenticator); - } - } catch (AuthenticationException e) { - incrementAuthErrorMetric(); - throw e; - } - switch (protocolVersion) { - case V1: - if (authenticator instanceof ProtocolV1Authenticator) - return authenticateV1(authenticator, protocolVersion, initExecutor); - else - // DSE 3.x always uses SASL authentication backported from protocol v2 - return authenticateV2(authenticator, protocolVersion, initExecutor); - case V2: - case V3: - case V4: - case V5: - case V6: - return authenticateV2(authenticator, protocolVersion, initExecutor); - default: - throw defunct(protocolVersion.unsupported()); - } - default: - throw new TransportException( - endPoint, - String.format( - "Unexpected %s response message from server to a STARTUP message", - response.type)); - } - } - }; - } - - // Due to C* gossip bugs, system.peers may report nodes that are gone from the cluster. - // If these nodes have been recommissionned to another cluster and are up, nothing prevents the - // driver from connecting - // to them. So we check that the cluster the node thinks it belongs to is our cluster (JAVA-397). - private ListenableFuture checkClusterName( - ProtocolVersion protocolVersion, final Executor executor) { - final String expected = factory.manager.metadata.clusterName; - - // At initialization, the cluster is not known yet - if (expected == null) { - markInitialized(); - return MoreFutures.VOID_SUCCESS; - } - - DefaultResultSetFuture clusterNameFuture = - new DefaultResultSetFuture( - null, - protocolVersion, - new Requests.Query("select cluster_name from system.local where key = 'local'")); - try { - write(clusterNameFuture); - return GuavaCompatibility.INSTANCE.transformAsync( - clusterNameFuture, - new AsyncFunction() { - @Override - public ListenableFuture apply(ResultSet rs) throws Exception { - Row row = rs.one(); - String actual = row.getString("cluster_name"); - if (!expected.equals(actual)) - throw new ClusterNameMismatchException(endPoint, actual, expected); - markInitialized(); - return MoreFutures.VOID_SUCCESS; - } - }, - executor); - } catch (Exception e) { - return Futures.immediateFailedFuture(e); - } - } - - private ListenableFuture getProductType(Responses.Supported response) { - if (response.supported.containsKey("PRODUCT_TYPE") - && response.supported.get("PRODUCT_TYPE").size() > 0) { - return Futures.immediateFuture(response.supported.get("PRODUCT_TYPE").get(0)); - } else { - return Futures.immediateFuture(""); - } - } - - private void markInitialized() { - isInitialized = true; - Host.statesLogger.debug("[{}] {} Transport initialized, connection ready", endPoint, this); - } - - private ListenableFuture authenticateV1( - Authenticator authenticator, final ProtocolVersion protocolVersion, final Executor executor) { - Requests.Credentials creds = - new Requests.Credentials(((ProtocolV1Authenticator) authenticator).getCredentials()); - try { - Future authResponseFuture = write(creds); - return GuavaCompatibility.INSTANCE.transformAsync( - authResponseFuture, - new AsyncFunction() { - @Override - public ListenableFuture apply(Message.Response authResponse) throws Exception { - switch (authResponse.type) { - case READY: - return checkClusterName(protocolVersion, executor); - case ERROR: - incrementAuthErrorMetric(); - throw new AuthenticationException( - endPoint, ((Responses.Error) authResponse).message); - default: - throw new TransportException( - endPoint, - String.format( - "Unexpected %s response message from server to a CREDENTIALS message", - authResponse.type)); - } - } - }, - executor); - } catch (Exception e) { - return Futures.immediateFailedFuture(e); - } - } - - private ListenableFuture authenticateV2( - final Authenticator authenticator, - final ProtocolVersion protocolVersion, - final Executor executor) { - byte[] initialResponse = authenticator.initialResponse(); - if (null == initialResponse) initialResponse = EMPTY_BYTE_ARRAY; - - try { - Future authResponseFuture = write(new Requests.AuthResponse(initialResponse)); - return GuavaCompatibility.INSTANCE.transformAsync( - authResponseFuture, onV2AuthResponse(authenticator, protocolVersion, executor), executor); - } catch (Exception e) { - return Futures.immediateFailedFuture(e); - } - } - - private AsyncFunction onV2AuthResponse( - final Authenticator authenticator, - final ProtocolVersion protocolVersion, - final Executor executor) { - return new AsyncFunction() { - @Override - public ListenableFuture apply(Message.Response authResponse) throws Exception { - switch (authResponse.type) { - case AUTH_SUCCESS: - logger.trace("{} Authentication complete", this); - authenticator.onAuthenticationSuccess(((Responses.AuthSuccess) authResponse).token); - return checkClusterName(protocolVersion, executor); - case AUTH_CHALLENGE: - byte[] responseToServer = - authenticator.evaluateChallenge(((Responses.AuthChallenge) authResponse).token); - if (responseToServer == null) { - // If we generate a null response, then authentication has completed, proceed without - // sending a further response back to the server. - logger.trace("{} Authentication complete (No response to server)", this); - return checkClusterName(protocolVersion, executor); - } else { - // Otherwise, send the challenge response back to the server - logger.trace("{} Sending Auth response to challenge", this); - Future nextResponseFuture = write(new Requests.AuthResponse(responseToServer)); - return GuavaCompatibility.INSTANCE.transformAsync( - nextResponseFuture, - onV2AuthResponse(authenticator, protocolVersion, executor), - executor); - } - case ERROR: - // This is not very nice, but we're trying to identify if we - // attempted v2 auth against a server which only supports v1 - // The AIOOBE indicates that the server didn't recognise the - // initial AuthResponse message - String message = ((Responses.Error) authResponse).message; - if (message.startsWith("java.lang.ArrayIndexOutOfBoundsException: 15")) - message = - String.format( - "Cannot use authenticator %s with protocol version 1, " - + "only plain text authentication is supported with this protocol version", - authenticator); - incrementAuthErrorMetric(); - throw new AuthenticationException(endPoint, message); - default: - throw new TransportException( - endPoint, - String.format( - "Unexpected %s response message from server to authentication message", - authResponse.type)); - } - } - }; - } - - private void incrementAuthErrorMetric() { - if (factory.manager.configuration.getMetricsOptions().isEnabled()) { - factory.manager.metrics.getErrorMetrics().getAuthenticationErrors().inc(); - } - } - - private boolean isUnsupportedProtocolVersion(Responses.Error error) { - // Testing for a specific string is a tad fragile but well, we don't have much choice - // C* 2.1 reports a server error instead of protocol error, see CASSANDRA-9451 - return (error.code == ExceptionCode.PROTOCOL_ERROR || error.code == ExceptionCode.SERVER_ERROR) - && (error.message.contains("Invalid or unsupported protocol version") - // JAVA-2924: server is behind driver and considers the proposed version as beta - || error.message.contains("Beta version of the protocol used")); - } - - private UnsupportedProtocolVersionException unsupportedProtocolVersionException( - ProtocolVersion triedVersion, ProtocolVersion serverProtocolVersion) { - UnsupportedProtocolVersionException e = - new UnsupportedProtocolVersionException(endPoint, triedVersion, serverProtocolVersion); - logger.debug(e.getMessage()); - return e; - } - - boolean isDefunct() { - return isDefunct.get(); - } - - int maxAvailableStreams() { - return dispatcher.streamIdHandler.maxAvailableStreams(); - } - - E defunct(E e) { - if (isDefunct.compareAndSet(false, true)) { - - if (Host.statesLogger.isTraceEnabled()) Host.statesLogger.trace("Defuncting " + this, e); - else if (Host.statesLogger.isDebugEnabled()) - Host.statesLogger.debug("Defuncting {} because: {}", this, e.getMessage()); - - Host host = getHost(); - if (host != null) { - // Sometimes close() can be called before defunct(); avoid decrementing the connection count - // twice, but - // we still want to signal the error to the conviction policy. - boolean decrement = signaled.compareAndSet(false, true); - - boolean hostDown = host.convictionPolicy.signalConnectionFailure(this, decrement); - if (hostDown) { - factory.manager.signalHostDown(host, host.wasJustAdded()); - } else { - notifyOwnerWhenDefunct(); - } - } - - // Force the connection to close to make sure the future completes. Otherwise force() might - // never get called and - // threads will wait on the future forever. - // (this also errors out pending handlers) - closeAsync().force(); - } - return e; - } - - private void notifyOwnerWhenDefunct() { - // If an error happens during initialization, the owner will detect it and take appropriate - // action - if (!isInitialized) return; - - Owner owner = this.ownerRef.get(); - if (owner != null) owner.onConnectionDefunct(this); - } - - String keyspace() { - return targetKeyspace.get().keyspace; - } - - void setKeyspace(String keyspace) throws ConnectionException { - if (keyspace == null) return; - - if (MoreObjects.equal(keyspace(), keyspace)) return; - - try { - Uninterruptibles.getUninterruptibly(setKeyspaceAsync(keyspace)); - } catch (ConnectionException e) { - throw defunct(e); - } catch (BusyConnectionException e) { - logger.warn( - "Tried to set the keyspace on busy {}. " - + "This should not happen but is not critical (it will be retried)", - this); - throw new ConnectionException(endPoint, "Tried to set the keyspace on busy connection"); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof OperationTimedOutException) { - // Rethrow so that the caller doesn't try to use the connection, but do not defunct as we - // don't want to mark down - logger.warn( - "Timeout while setting keyspace on {}. " - + "This should not happen but is not critical (it will be retried)", - this); - throw new ConnectionException(endPoint, "Timeout while setting keyspace on connection"); - } else { - throw defunct(new ConnectionException(endPoint, "Error while setting keyspace", cause)); - } - } - } - - ListenableFuture setKeyspaceAsync(final String keyspace) - throws ConnectionException, BusyConnectionException { - SetKeyspaceAttempt existingAttempt = targetKeyspace.get(); - if (MoreObjects.equal(existingAttempt.keyspace, keyspace)) return existingAttempt.future; - - final SettableFuture ksFuture = SettableFuture.create(); - final SetKeyspaceAttempt attempt = new SetKeyspaceAttempt(keyspace, ksFuture); - - // Check for an existing keyspace attempt. - while (true) { - existingAttempt = targetKeyspace.get(); - // if existing attempts' keyspace matches what we are trying to set, use it. - if (attempt.equals(existingAttempt)) { - return existingAttempt.future; - } else if (!existingAttempt.future.isDone()) { - // If the existing attempt is still in flight, fail this attempt immediately. - ksFuture.setException( - new DriverException( - "Aborting attempt to set keyspace to '" - + keyspace - + "' since there is already an in flight attempt to set keyspace to '" - + existingAttempt.keyspace - + "'. " - + "This can happen if you try to USE different keyspaces from the same session simultaneously.")); - return ksFuture; - } else if (targetKeyspace.compareAndSet(existingAttempt, attempt)) { - // Otherwise, if the existing attempt is done, start a new set keyspace attempt for the new - // keyspace. - logger.debug("{} Setting keyspace {}", this, keyspace); - // Note: we quote the keyspace below, because the name is the one coming from Cassandra, so - // it's in the right case already - Future future = write(new Requests.Query("USE \"" + keyspace + '"')); - GuavaCompatibility.INSTANCE.addCallback( - future, - new FutureCallback() { - - @Override - public void onSuccess(Message.Response response) { - if (response instanceof SetKeyspace) { - logger.debug("{} Keyspace set to {}", Connection.this, keyspace); - ksFuture.set(Connection.this); - } else { - // Unset this attempt so new attempts may be made for the same keyspace. - targetKeyspace.compareAndSet(attempt, defaultKeyspaceAttempt); - if (response.type == ERROR) { - Responses.Error error = (Responses.Error) response; - ksFuture.setException(defunct(error.asException(endPoint))); - } else { - ksFuture.setException( - defunct( - new DriverInternalError( - "Unexpected response while setting keyspace: " + response))); - } - } - } - - @Override - public void onFailure(Throwable t) { - targetKeyspace.compareAndSet(attempt, defaultKeyspaceAttempt); - ksFuture.setException(t); - } - }, - factory.manager.configuration.getPoolingOptions().getInitializationExecutor()); - - return ksFuture; - } - } - } - - /** - * Write a request on this connection. - * - * @param request the request to send - * @return a future on the server response - * @throws ConnectionException if the connection is closed - * @throws TransportException if an I/O error while sending the request - */ - Future write(Message.Request request) throws ConnectionException, BusyConnectionException { - Future future = new Future(request); - write(future); - return future; - } - - ResponseHandler write(ResponseCallback callback) - throws ConnectionException, BusyConnectionException { - return write(callback, -1, true); - } - - ResponseHandler write( - ResponseCallback callback, long statementReadTimeoutMillis, boolean startTimeout) - throws ConnectionException, BusyConnectionException { - - ResponseHandler handler = new ResponseHandler(this, statementReadTimeoutMillis, callback); - dispatcher.add(handler); - - Message.Request request = callback.request().setStreamId(handler.streamId); - - /* - * We check for close/defunct *after* having set the handler because closing/defuncting - * will set their flag and then error out handler if need. So, by doing the check after - * having set the handler, we guarantee that even if we race with defunct/close, we may - * never leave a handler that won't get an answer or be errored out. - */ - if (isDefunct.get()) { - dispatcher.removeHandler(handler, true); - throw new ConnectionException(endPoint, "Write attempt on defunct connection"); - } - - if (isClosed()) { - dispatcher.removeHandler(handler, true); - throw new ConnectionException(endPoint, "Connection has been closed"); - } - - logger.trace("{}, stream {}, writing request {}", this, request.getStreamId(), request); - writer.incrementAndGet(); - - if (DISABLE_COALESCING) { - channel.writeAndFlush(request).addListener(writeHandler(request, handler)); - } else { - flush(new FlushItem(channel, request, writeHandler(request, handler))); - } - if (startTimeout) handler.startTimeout(); - - return handler; - } - - private ChannelFutureListener writeHandler( - final Message.Request request, final ResponseHandler handler) { - return new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture writeFuture) { - - writer.decrementAndGet(); - - if (!writeFuture.isSuccess()) { - logger.debug( - "{}, stream {}, Error writing request {}", - Connection.this, - request.getStreamId(), - request); - // Remove this handler from the dispatcher so it don't get notified of the error - // twice (we will fail that method already) - dispatcher.removeHandler(handler, true); - - final ConnectionException ce; - if (writeFuture.cause() instanceof java.nio.channels.ClosedChannelException) { - ce = new TransportException(endPoint, "Error writing: Closed channel"); - } else { - ce = new TransportException(endPoint, "Error writing", writeFuture.cause()); - } - final long latency = System.nanoTime() - handler.startTime; - // This handler is executed while holding the writeLock of the channel. - // defunct might close the pool, which will close all of its connections; closing a - // connection also - // requires its writeLock. - // Therefore if multiple connections in the same pool get a write error, they could - // deadlock; - // we run defunct on a separate thread to avoid that. - ListeningExecutorService executor = factory.manager.executor; - if (!executor.isShutdown()) - executor.execute( - new Runnable() { - @Override - public void run() { - handler.callback.onException( - Connection.this, defunct(ce), latency, handler.retryCount); - } - }); - } else { - logger.trace( - "{}, stream {}, request sent successfully", Connection.this, request.getStreamId()); - } - } - }; - } - - boolean hasOwner() { - return this.ownerRef.get() != null; - } - - /** @return whether the connection was already associated with an owner */ - boolean setOwner(Owner owner) { - return ownerRef.compareAndSet(null, owner); - } - - /** - * If the connection is part of a pool, return it to the pool. The connection should generally not - * be reused after that. - */ - void release(boolean busy) { - Owner owner = ownerRef.get(); - if (owner instanceof HostConnectionPool) - ((HostConnectionPool) owner).returnConnection(this, busy); - } - - void release() { - release(false); - } - - boolean isClosed() { - return closeFuture.get() != null; - } - - /** - * Closes the connection: no new writes will be accepted after this method has returned. - * - *

However, a closed connection might still have ongoing queries awaiting for their result. - * When all these ongoing queries have completed, the underlying channel will be closed; we refer - * to this final state as "terminated". - * - * @return a future that will complete once the connection has terminated. - * @see #tryTerminate(boolean) - */ - CloseFuture closeAsync() { - - ConnectionCloseFuture future = new ConnectionCloseFuture(); - if (!closeFuture.compareAndSet(null, future)) { - // close had already been called, return the existing future - return closeFuture.get(); - } - - logger.debug("{} closing connection", this); - - // Only signal if defunct hasn't done it already - if (signaled.compareAndSet(false, true)) { - Host host = getHost(); - if (host != null) { - host.convictionPolicy.signalConnectionClosed(this); - } - } - - boolean terminated = tryTerminate(false); - if (!terminated) { - // The time by which all pending requests should have normally completed (use twice the read - // timeout for a generous - // estimate -- note that this does not cover the eventuality that read timeout is updated - // dynamically, but we can live - // with that). - long terminateTime = System.currentTimeMillis() + 2 * factory.getReadTimeoutMillis(); - factory.reaper.register(this, terminateTime); - } - return future; - } - - private Host getHost() { - Metadata metadata = factory.manager.metadata; - Host host = metadata.getHost(endPoint); - // During init the host might not be in metatada.hosts yet, try the contact points - if (host == null) { - host = metadata.getContactPoint(endPoint); - } - return host; - } - - /** - * Tries to terminate a closed connection, i.e. release system resources. - * - *

This is called both by "normal" code and by {@link Cluster.ConnectionReaper}. - * - * @param force whether to proceed if there are still outstanding requests. - * @return whether the connection has actually terminated. - * @see #closeAsync() - */ - boolean tryTerminate(boolean force) { - assert isClosed(); - ConnectionCloseFuture future = closeFuture.get(); - - if (future.isDone()) { - logger.debug("{} has already terminated", this); - return true; - } else { - if (force || dispatcher.pending.isEmpty()) { - if (force) - logger.warn( - "Forcing termination of {}. This should not happen and is likely a bug, please report.", - this); - future.force(); - return true; - } else { - logger.debug("Not terminating {}: there are still pending requests", this); - return false; - } - } - } - - @Override - public String toString() { - return String.format( - "Connection[%s, inFlight=%d, closed=%b]", name, inFlight.get(), isClosed()); - } - - static class Factory { - - final Timer timer; - - final EventLoopGroup eventLoopGroup; - private final Class channelClass; - - private final ChannelGroup allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); - - private final ConcurrentMap idGenerators = - new ConcurrentHashMap(); - final DefaultResponseHandler defaultHandler; - final Cluster.Manager manager; - final Cluster.ConnectionReaper reaper; - final Configuration configuration; - - final AuthProvider authProvider; - private volatile boolean isShutdown; - - volatile ProtocolVersion protocolVersion; - private final NettyOptions nettyOptions; - - Factory(Cluster.Manager manager, Configuration configuration) { - this.defaultHandler = manager; - this.manager = manager; - this.reaper = manager.reaper; - this.configuration = configuration; - this.authProvider = configuration.getProtocolOptions().getAuthProvider(); - this.protocolVersion = configuration.getProtocolOptions().initialProtocolVersion; - this.nettyOptions = configuration.getNettyOptions(); - this.eventLoopGroup = - nettyOptions.eventLoopGroup( - manager - .configuration - .getThreadingOptions() - .createThreadFactory(manager.clusterName, "nio-worker")); - this.channelClass = nettyOptions.channelClass(); - this.timer = - nettyOptions.timer( - manager - .configuration - .getThreadingOptions() - .createThreadFactory(manager.clusterName, "timeouter")); - } - - int getPort() { - return configuration.getProtocolOptions().getPort(); - } - - /** - * Opens a new connection to the node this factory points to. - * - * @return the newly created (and initialized) connection. - * @throws ConnectionException if connection attempt fails. - */ - Connection open(Host host) - throws ConnectionException, InterruptedException, UnsupportedProtocolVersionException, - ClusterNameMismatchException { - EndPoint endPoint = host.getEndPoint(); - - if (isShutdown) throw new ConnectionException(endPoint, "Connection factory is shut down"); - - host.convictionPolicy.signalConnectionsOpening(1); - Connection connection = new Connection(buildConnectionName(host), endPoint, this); - // This method opens the connection synchronously, so wait until it's initialized - try { - connection.initAsync().get(); - return connection; - } catch (ExecutionException e) { - throw launderAsyncInitException(e); - } - } - - /** Same as open, but associate the created connection to the provided connection pool. */ - Connection open(HostConnectionPool pool) - throws ConnectionException, InterruptedException, UnsupportedProtocolVersionException, - ClusterNameMismatchException { - pool.host.convictionPolicy.signalConnectionsOpening(1); - Connection connection = - new Connection(buildConnectionName(pool.host), pool.host.getEndPoint(), this, pool); - try { - connection.initAsync().get(); - return connection; - } catch (ExecutionException e) { - throw launderAsyncInitException(e); - } - } - - /** - * Creates new connections and associate them to the provided connection pool, but does not - * start them. - */ - List newConnections(HostConnectionPool pool, int count) { - pool.host.convictionPolicy.signalConnectionsOpening(count); - List connections = Lists.newArrayListWithCapacity(count); - for (int i = 0; i < count; i++) - connections.add( - new Connection(buildConnectionName(pool.host), pool.host.getEndPoint(), this, pool)); - return connections; - } - - private String buildConnectionName(Host host) { - return host.getEndPoint().toString() + '-' + getIdGenerator(host).getAndIncrement(); - } - - static RuntimeException launderAsyncInitException(ExecutionException e) - throws ConnectionException, InterruptedException, UnsupportedProtocolVersionException, - ClusterNameMismatchException { - Throwable t = e.getCause(); - if (t instanceof ConnectionException) throw (ConnectionException) t; - if (t instanceof InterruptedException) throw (InterruptedException) t; - if (t instanceof UnsupportedProtocolVersionException) - throw (UnsupportedProtocolVersionException) t; - if (t instanceof ClusterNameMismatchException) throw (ClusterNameMismatchException) t; - if (t instanceof DriverException) throw (DriverException) t; - if (t instanceof Error) throw (Error) t; - - return new RuntimeException("Unexpected exception during connection initialization", t); - } - - private AtomicInteger getIdGenerator(Host host) { - AtomicInteger g = idGenerators.get(host); - if (g == null) { - g = new AtomicInteger(1); - AtomicInteger old = idGenerators.putIfAbsent(host, g); - if (old != null) g = old; - } - return g; - } - - long getReadTimeoutMillis() { - return configuration.getSocketOptions().getReadTimeoutMillis(); - } - - private Bootstrap newBootstrap() { - Bootstrap b = new Bootstrap(); - b.group(eventLoopGroup).channel(channelClass); - - SocketOptions options = configuration.getSocketOptions(); - - b.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, options.getConnectTimeoutMillis()); - Boolean keepAlive = options.getKeepAlive(); - if (keepAlive != null) b.option(ChannelOption.SO_KEEPALIVE, keepAlive); - Boolean reuseAddress = options.getReuseAddress(); - if (reuseAddress != null) b.option(ChannelOption.SO_REUSEADDR, reuseAddress); - Integer soLinger = options.getSoLinger(); - if (soLinger != null) b.option(ChannelOption.SO_LINGER, soLinger); - Boolean tcpNoDelay = options.getTcpNoDelay(); - if (tcpNoDelay != null) b.option(ChannelOption.TCP_NODELAY, tcpNoDelay); - Integer receiveBufferSize = options.getReceiveBufferSize(); - if (receiveBufferSize != null) b.option(ChannelOption.SO_RCVBUF, receiveBufferSize); - Integer sendBufferSize = options.getSendBufferSize(); - if (sendBufferSize != null) b.option(ChannelOption.SO_SNDBUF, sendBufferSize); - - nettyOptions.afterBootstrapInitialized(b); - return b; - } - - void shutdown() { - // Make sure we skip creating connection from now on. - isShutdown = true; - - // All channels should be closed already, we call this just to be sure. And we know - // we're not on an I/O thread or anything, so just call await. - allChannels.close().awaitUninterruptibly(); - - nettyOptions.onClusterClose(eventLoopGroup); - nettyOptions.onClusterClose(timer); - } - } - - private static final class Flusher implements Runnable { - final WeakReference eventLoopRef; - final Queue queued = new ConcurrentLinkedQueue(); - final AtomicBoolean running = new AtomicBoolean(false); - final HashSet channels = new HashSet(); - - private Flusher(EventLoop eventLoop) { - this.eventLoopRef = new WeakReference(eventLoop); - } - - void start() { - if (!running.get() && running.compareAndSet(false, true)) { - EventLoop eventLoop = eventLoopRef.get(); - if (eventLoop != null) eventLoop.execute(this); - } - } - - @Override - public void run() { - - FlushItem flush; - while (null != (flush = queued.poll())) { - Channel channel = flush.channel; - if (channel.isActive()) { - channels.add(channel); - channel.write(flush.request).addListener(flush.listener); - } - } - - // Always flush what we have (don't artificially delay to try to coalesce more messages) - for (Channel channel : channels) channel.flush(); - channels.clear(); - - // either reschedule or cancel - running.set(false); - if (queued.isEmpty() || !running.compareAndSet(false, true)) return; - - EventLoop eventLoop = eventLoopRef.get(); - if (eventLoop != null && !eventLoop.isShuttingDown()) { - if (FLUSHER_SCHEDULE_PERIOD_NS > 0) { - eventLoop.schedule(this, FLUSHER_SCHEDULE_PERIOD_NS, TimeUnit.NANOSECONDS); - } else { - eventLoop.execute(this); - } - } - } - } - - private static final ConcurrentMap flusherLookup = - new MapMaker().concurrencyLevel(16).weakKeys().makeMap(); - - private static class FlushItem { - final Channel channel; - final Object request; - final ChannelFutureListener listener; - - private FlushItem(Channel channel, Object request, ChannelFutureListener listener) { - this.channel = channel; - this.request = request; - this.listener = listener; - } - } - - private void flush(FlushItem item) { - EventLoop loop = item.channel.eventLoop(); - Flusher flusher = flusherLookup.get(loop); - if (flusher == null) { - Flusher alt = flusherLookup.putIfAbsent(loop, flusher = new Flusher(loop)); - if (alt != null) flusher = alt; - } - - flusher.queued.add(item); - flusher.start(); - } - - class Dispatcher extends SimpleChannelInboundHandler { - - final StreamIdGenerator streamIdHandler; - private final ConcurrentMap pending = - new ConcurrentHashMap(); - - Dispatcher() { - ProtocolVersion protocolVersion = factory.protocolVersion; - if (protocolVersion == null) { - // This happens for the first control connection because the protocol version has not been - // negotiated yet. - protocolVersion = ProtocolVersion.V2; - } - streamIdHandler = StreamIdGenerator.newInstance(protocolVersion); - } - - void add(ResponseHandler handler) { - ResponseHandler old = pending.put(handler.streamId, handler); - assert old == null; - } - - void removeHandler(ResponseHandler handler, boolean releaseStreamId) { - - // If we don't release the ID, mark first so that we can rely later on the fact that if - // we receive a response for an ID with no handler, it's that this ID has been marked. - if (!releaseStreamId) streamIdHandler.mark(handler.streamId); - - // If a RequestHandler is cancelled right when the response arrives, this method (called with - // releaseStreamId=false) will race with messageReceived. - // messageReceived could have already released the streamId, which could have already been - // reused by another request. We must not remove the handler - // if it's not ours, because that would cause the other request to hang forever. - boolean removed = pending.remove(handler.streamId, handler); - if (!removed) { - // We raced, so if we marked the streamId above, that was wrong. - if (!releaseStreamId) streamIdHandler.unmark(handler.streamId); - return; - } - handler.cancelTimeout(); - - if (releaseStreamId) streamIdHandler.release(handler.streamId); - - if (isClosed()) tryTerminate(false); - } - - @Override - protected void channelRead0(ChannelHandlerContext ctx, Message.Response response) - throws Exception { - int streamId = response.getStreamId(); - - if (logger.isTraceEnabled()) - logger.trace( - "{}, stream {}, received: {}", Connection.this, streamId, asDebugString(response)); - - if (streamId < 0) { - factory.defaultHandler.handle(response); - return; - } - - ResponseHandler handler = pending.remove(streamId); - streamIdHandler.release(streamId); - if (handler == null) { - /* - * During normal operation, we should not receive responses for which we don't have a handler. There is - * two cases however where this can happen: - * 1) The connection has been defuncted due to some internal error and we've raced between removing the - * handler and actually closing the connection; since the original error has been logged, we're fine - * ignoring this completely. - * 2) This request has timed out. In that case, we've already switched to another host (or errored out - * to the user). So log it for debugging purpose, but it's fine ignoring otherwise. - */ - streamIdHandler.unmark(streamId); - if (logger.isDebugEnabled()) - logger.debug( - "{} Response received on stream {} but no handler set anymore (either the request has " - + "timed out or it was closed due to another error). Received message is {}", - Connection.this, - streamId, - asDebugString(response)); - return; - } - handler.cancelTimeout(); - handler.callback.onSet( - Connection.this, response, System.nanoTime() - handler.startTime, handler.retryCount); - - // If we happen to be closed and we're the last outstanding request, we need to terminate the - // connection - // (note: this is racy as the signaling can be called more than once, but that's not a - // problem) - if (isClosed()) tryTerminate(false); - } - - @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { - if (isInitialized - && !isClosed() - && evt instanceof IdleStateEvent - && ((IdleStateEvent) evt).state() == READER_IDLE) { - logger.debug( - "{} was inactive for {} seconds, sending heartbeat", - Connection.this, - factory.configuration.getPoolingOptions().getHeartbeatIntervalSeconds()); - write(HEARTBEAT_CALLBACK); - } - } - - // Make sure we don't print huge responses in debug/error logs. - private String asDebugString(Object obj) { - if (obj == null) return "null"; - - String msg = obj.toString(); - if (msg.length() < 500) return msg; - - return msg.substring(0, 500) + "... [message of size " + msg.length() + " truncated]"; - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - if (logger.isDebugEnabled()) - logger.debug(String.format("%s connection error", Connection.this), cause); - - // Ignore exception while writing, this will be handled by write() directly - if (writer.get() > 0) return; - - if (cause instanceof DecoderException) { - Throwable error = cause.getCause(); - // Special case, if we encountered a FrameTooLongException, raise exception on handler and - // don't defunct it since - // the connection is in an ok state. - if (error instanceof FrameTooLongException) { - FrameTooLongException ftle = (FrameTooLongException) error; - int streamId = ftle.getStreamId(); - ResponseHandler handler = pending.remove(streamId); - streamIdHandler.release(streamId); - if (handler == null) { - streamIdHandler.unmark(streamId); - if (logger.isDebugEnabled()) - logger.debug( - "{} FrameTooLongException received on stream {} but no handler set anymore (either the request has " - + "timed out or it was closed due to another error).", - Connection.this, - streamId); - return; - } - handler.cancelTimeout(); - handler.callback.onException( - Connection.this, ftle, System.nanoTime() - handler.startTime, handler.retryCount); - return; - } else if (error instanceof CrcMismatchException) { - // Fall back to the defunct call below, but we want a clear warning in the logs - logger.warn("CRC mismatch while decoding a response, dropping the connection", error); - } - } - defunct( - new TransportException( - endPoint, String.format("Unexpected exception triggered (%s)", cause), cause)); - } - - void errorOutAllHandler(ConnectionException ce) { - Iterator iter = pending.values().iterator(); - while (iter.hasNext()) { - ResponseHandler handler = iter.next(); - handler.cancelTimeout(); - handler.callback.onException( - Connection.this, ce, System.nanoTime() - handler.startTime, handler.retryCount); - iter.remove(); - } - } - } - - private class ChannelCloseListener implements ChannelFutureListener { - @Override - public void operationComplete(ChannelFuture future) throws Exception { - // If we've closed the channel client side then we don't really want to defunct the - // connection, but - // if there is remaining thread waiting on us, we still want to wake them up - if (!isInitialized || isClosed()) { - dispatcher.errorOutAllHandler(new TransportException(endPoint, "Channel has been closed")); - // we still want to force so that the future completes - Connection.this.closeAsync().force(); - } else defunct(new TransportException(endPoint, "Channel has been closed")); - } - } - - private static final ResponseCallback HEARTBEAT_CALLBACK = - new ResponseCallback() { - - @Override - public Message.Request request() { - return new Requests.Options(); - } - - @Override - public int retryCount() { - return 0; // no retries here - } - - @Override - public void onSet( - Connection connection, Message.Response response, long latency, int retryCount) { - switch (response.type) { - case SUPPORTED: - logger.debug("{} heartbeat query succeeded", connection); - break; - default: - fail( - connection, - new ConnectionException( - connection.endPoint, "Unexpected heartbeat response: " + response)); - } - } - - @Override - public void onException( - Connection connection, Exception exception, long latency, int retryCount) { - // Nothing to do: the connection is already defunct if we arrive here - } - - @Override - public boolean onTimeout(Connection connection, long latency, int retryCount) { - fail( - connection, - new ConnectionException(connection.endPoint, "Heartbeat query timed out")); - return true; - } - - private void fail(Connection connection, Exception e) { - connection.defunct(e); - } - }; - - private class ConnectionCloseFuture extends CloseFuture { - - @Override - public ConnectionCloseFuture force() { - // Note: we must not call releaseExternalResources on the bootstrap, because this shutdown the - // executors, which are shared - - // This method can be thrown during initialization, at which point channel is not yet set. - // This is ok. - if (channel == null) { - set(null); - return this; - } - - // We're going to close this channel. If anyone is waiting on that connection, we should - // defunct it otherwise it'll wait - // forever. In general this won't happen since we get there only when all ongoing query are - // done, but this can happen - // if the shutdown is forced. This is a no-op if there is no handler set anymore. - dispatcher.errorOutAllHandler(new TransportException(endPoint, "Connection has been closed")); - - ChannelFuture future = channel.close(); - future.addListener( - new ChannelFutureListener() { - @Override - public void operationComplete(ChannelFuture future) { - factory.allChannels.remove(channel); - if (future.cause() != null) { - logger.warn("Error closing channel", future.cause()); - ConnectionCloseFuture.this.setException(future.cause()); - } else ConnectionCloseFuture.this.set(null); - } - }); - return this; - } - } - - private class SetKeyspaceAttempt { - private final String keyspace; - private final ListenableFuture future; - - SetKeyspaceAttempt(String keyspace, ListenableFuture future) { - this.keyspace = keyspace; - this.future = future; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof SetKeyspaceAttempt)) return false; - - SetKeyspaceAttempt that = (SetKeyspaceAttempt) o; - - return keyspace != null ? keyspace.equals(that.keyspace) : that.keyspace == null; - } - - @Override - public int hashCode() { - return keyspace != null ? keyspace.hashCode() : 0; - } - } - - static class Future extends AbstractFuture implements RequestHandler.Callback { - - private final Message.Request request; - private volatile EndPoint endPoint; - - Future(Message.Request request) { - this.request = request; - } - - @Override - public void register(RequestHandler handler) { - // noop, we don't care about the handler here so far - } - - @Override - public Message.Request request() { - return request; - } - - @Override - public int retryCount() { - // This is ignored, as there is no retry logic in this class - return 0; - } - - @Override - public void onSet( - Connection connection, - Message.Response response, - ExecutionInfo info, - Statement statement, - long latency) { - onSet(connection, response, latency, 0); - } - - @Override - public void onSet( - Connection connection, Message.Response response, long latency, int retryCount) { - this.endPoint = connection.endPoint; - super.set(response); - } - - @Override - public void onException( - Connection connection, Exception exception, long latency, int retryCount) { - // If all nodes are down, we will get a null connection here. This is fine, if we have - // an exception, consumers shouldn't assume the address is not null. - if (connection != null) this.endPoint = connection.endPoint; - super.setException(exception); - } - - @Override - public boolean onTimeout(Connection connection, long latency, int retryCount) { - assert connection - != null; // We always timeout on a specific connection, so this shouldn't be null - this.endPoint = connection.endPoint; - return super.setException(new OperationTimedOutException(connection.endPoint)); - } - - EndPoint getEndPoint() { - return endPoint; - } - } - - interface ResponseCallback { - Message.Request request(); - - int retryCount(); - - void onSet(Connection connection, Message.Response response, long latency, int retryCount); - - void onException(Connection connection, Exception exception, long latency, int retryCount); - - boolean onTimeout(Connection connection, long latency, int retryCount); - } - - static class ResponseHandler { - - final Connection connection; - final int streamId; - final ResponseCallback callback; - final int retryCount; - private final long readTimeoutMillis; - - private final long startTime; - private volatile Timeout timeout; - - private final AtomicBoolean isCancelled = new AtomicBoolean(); - - ResponseHandler( - Connection connection, long statementReadTimeoutMillis, ResponseCallback callback) - throws BusyConnectionException { - this.connection = connection; - this.readTimeoutMillis = - (statementReadTimeoutMillis >= 0) - ? statementReadTimeoutMillis - : connection.factory.getReadTimeoutMillis(); - this.streamId = connection.dispatcher.streamIdHandler.next(); - if (streamId == -1) throw new BusyConnectionException(connection.endPoint); - this.callback = callback; - this.retryCount = callback.retryCount(); - - this.startTime = System.nanoTime(); - } - - void startTimeout() { - this.timeout = - this.readTimeoutMillis <= 0 - ? null - : connection.factory.timer.newTimeout( - onTimeoutTask(), this.readTimeoutMillis, TimeUnit.MILLISECONDS); - } - - void cancelTimeout() { - if (timeout != null) timeout.cancel(); - } - - boolean cancelHandler() { - if (!isCancelled.compareAndSet(false, true)) return false; - - // We haven't really received a response: we want to remove the handle because we gave up on - // that - // request and there is no point in holding the handler, but we don't release the streamId. If - // we - // were, a new request could reuse that ID but get the answer to the request we just gave up - // on instead - // of its own answer, and we would have no way to detect that. - connection.dispatcher.removeHandler(this, false); - return true; - } - - private TimerTask onTimeoutTask() { - return new TimerTask() { - @Override - public void run(Timeout timeout) { - if (callback.onTimeout(connection, System.nanoTime() - startTime, retryCount)) - cancelHandler(); - } - }; - } - } - - interface DefaultResponseHandler { - void handle(Message.Response response); - } - - private static class Initializer extends ChannelInitializer { - // Stateless handlers - private static final Message.ProtocolDecoder messageDecoder = new Message.ProtocolDecoder(); - private static final Message.ProtocolEncoder messageEncoderV1 = - new Message.ProtocolEncoder(ProtocolVersion.V1); - private static final Message.ProtocolEncoder messageEncoderV2 = - new Message.ProtocolEncoder(ProtocolVersion.V2); - private static final Message.ProtocolEncoder messageEncoderV3 = - new Message.ProtocolEncoder(ProtocolVersion.V3); - private static final Message.ProtocolEncoder messageEncoderV4 = - new Message.ProtocolEncoder(ProtocolVersion.V4); - private static final Message.ProtocolEncoder messageEncoderV5 = - new Message.ProtocolEncoder(ProtocolVersion.V5); - private static final Message.ProtocolEncoder messageEncoderV6 = - new Message.ProtocolEncoder(ProtocolVersion.V6); - private static final Frame.Encoder frameEncoder = new Frame.Encoder(); - - private final ProtocolVersion protocolVersion; - private final Connection connection; - private final FrameCompressor compressor; - private final SSLOptions sslOptions; - private final NettyOptions nettyOptions; - private final ChannelHandler idleStateHandler; - private final CodecRegistry codecRegistry; - private final Metrics metrics; - - Initializer( - Connection connection, - ProtocolVersion protocolVersion, - FrameCompressor compressor, - SSLOptions sslOptions, - int heartBeatIntervalSeconds, - NettyOptions nettyOptions, - CodecRegistry codecRegistry, - Metrics metrics) { - this.connection = connection; - this.protocolVersion = protocolVersion; - this.compressor = compressor; - this.sslOptions = sslOptions; - this.nettyOptions = nettyOptions; - this.codecRegistry = codecRegistry; - this.idleStateHandler = new IdleStateHandler(heartBeatIntervalSeconds, 0, 0); - this.metrics = metrics; - } - - @Override - protected void initChannel(SocketChannel channel) throws Exception { - - // set the codec registry so that it can be accessed by ProtocolDecoder - channel.attr(Message.CODEC_REGISTRY_ATTRIBUTE_KEY).set(codecRegistry); - - ChannelPipeline pipeline = channel.pipeline(); - - if (sslOptions != null) { - SslHandler handler; - if (sslOptions instanceof ExtendedRemoteEndpointAwareSslOptions) { - handler = - ((ExtendedRemoteEndpointAwareSslOptions) sslOptions) - .newSSLHandler(channel, connection.endPoint); - - } else if (sslOptions instanceof RemoteEndpointAwareSSLOptions) { - handler = - ((RemoteEndpointAwareSSLOptions) sslOptions) - .newSSLHandler(channel, connection.endPoint.resolve()); - } else { - handler = sslOptions.newSSLHandler(channel); - } - pipeline.addLast("ssl", handler); - } - - // pipeline.addLast("debug", new LoggingHandler(LogLevel.INFO)); - - if (metrics != null) { - pipeline.addLast( - "inboundTrafficMeter", new InboundTrafficMeter(metrics.getBytesReceived())); - pipeline.addLast("outboundTrafficMeter", new OutboundTrafficMeter(metrics.getBytesSent())); - } - - pipeline.addLast("frameDecoder", new Frame.Decoder()); - pipeline.addLast("frameEncoder", frameEncoder); - - pipeline.addLast("framingFormatHandler", new FramingFormatHandler(connection.factory)); - - if (compressor != null - // Frame-level compression is only done in legacy protocol versions. In V5 and above, it - // happens at a higher level ("segment" that groups multiple frames), so never install - // those handlers. - && protocolVersion.compareTo(ProtocolVersion.V5) < 0) { - pipeline.addLast("frameDecompressor", new Frame.Decompressor(compressor)); - pipeline.addLast("frameCompressor", new Frame.Compressor(compressor)); - } - - pipeline.addLast("messageDecoder", messageDecoder); - pipeline.addLast("messageEncoder", messageEncoderFor(protocolVersion)); - - pipeline.addLast("idleStateHandler", idleStateHandler); - - pipeline.addLast("dispatcher", connection.dispatcher); - - nettyOptions.afterChannelInitialized(channel); - } - - private Message.ProtocolEncoder messageEncoderFor(ProtocolVersion version) { - switch (version) { - case V1: - return messageEncoderV1; - case V2: - return messageEncoderV2; - case V3: - return messageEncoderV3; - case V4: - return messageEncoderV4; - case V5: - return messageEncoderV5; - case V6: - return messageEncoderV6; - default: - throw new DriverInternalError("Unsupported protocol version " + protocolVersion); - } - } - } - - /** A component that "owns" a connection, and should be notified when it dies. */ - interface Owner { - void onConnectionDefunct(Connection connection); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ConsistencyLevel.java b/driver-core/src/main/java/com/datastax/driver/core/ConsistencyLevel.java deleted file mode 100644 index fbe378fc263..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ConsistencyLevel.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; - -public enum ConsistencyLevel { - ANY(0), - ONE(1), - TWO(2), - THREE(3), - QUORUM(4), - ALL(5), - LOCAL_QUORUM(6), - EACH_QUORUM(7), - SERIAL(8), - LOCAL_SERIAL(9), - LOCAL_ONE(10); - - // Used by the native protocol - final int code; - private static final ConsistencyLevel[] codeIdx; - - static { - int maxCode = -1; - for (ConsistencyLevel cl : ConsistencyLevel.values()) maxCode = Math.max(maxCode, cl.code); - codeIdx = new ConsistencyLevel[maxCode + 1]; - for (ConsistencyLevel cl : ConsistencyLevel.values()) { - if (codeIdx[cl.code] != null) throw new IllegalStateException("Duplicate code"); - codeIdx[cl.code] = cl; - } - } - - private ConsistencyLevel(int code) { - this.code = code; - } - - static ConsistencyLevel fromCode(int code) { - if (code < 0 || code >= codeIdx.length) - throw new DriverInternalError(String.format("Unknown code %d for a consistency level", code)); - return codeIdx[code]; - } - - /** - * Whether or not this consistency level applies to the local data-center only. - * - * @return whether this consistency level is {@code LOCAL_ONE}, {@code LOCAL_QUORUM}, or {@code - * LOCAL_SERIAL}. - */ - public boolean isDCLocal() { - return this == LOCAL_ONE || this == LOCAL_QUORUM || this == LOCAL_SERIAL; - } - - /** - * Whether or not this consistency level is serial, that is, applies only to the "paxos" phase of - * a Lightweight - * transaction. - * - *

Serial consistency levels are only meaningful when executing conditional updates ({@code - * INSERT}, {@code UPDATE} or {@code DELETE} statements with an {@code IF} condition). - * - *

Two consistency levels belong to this category: {@link #SERIAL} and {@link #LOCAL_SERIAL}. - * - * @return whether this consistency level is {@link #SERIAL} or {@link #LOCAL_SERIAL}. - * @see Statement#setSerialConsistencyLevel(ConsistencyLevel) - * @see Lightweight - * transactions - */ - public boolean isSerial() { - return this == SERIAL || this == LOCAL_SERIAL; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ControlConnection.java b/driver-core/src/main/java/com/datastax/driver/core/ControlConnection.java deleted file mode 100644 index 24f7b815c23..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ControlConnection.java +++ /dev/null @@ -1,1126 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.SchemaElement.KEYSPACE; - -import com.datastax.driver.core.exceptions.BusyConnectionException; -import com.datastax.driver.core.exceptions.ConnectionException; -import com.datastax.driver.core.exceptions.DriverException; -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.exceptions.InvalidQueryException; -import com.datastax.driver.core.exceptions.NoHostAvailableException; -import com.datastax.driver.core.exceptions.ServerError; -import com.datastax.driver.core.exceptions.UnsupportedProtocolVersionException; -import com.datastax.driver.core.utils.MoreFutures; -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -class ControlConnection implements Connection.Owner { - - private static final Logger logger = LoggerFactory.getLogger(ControlConnection.class); - - private static final boolean EXTENDED_PEER_CHECK = - SystemProperties.getBoolean("com.datastax.driver.EXTENDED_PEER_CHECK", true); - - private static final InetAddress bindAllAddress; - - static { - try { - bindAllAddress = InetAddress.getByAddress(new byte[4]); - } catch (UnknownHostException e) { - throw new RuntimeException(e); - } - } - - private static final String SELECT_PEERS = "SELECT * FROM system.peers"; - private static final String SELECT_PEERS_V2 = "SELECT * FROM system.peers_v2"; - private static final String SELECT_LOCAL = "SELECT * FROM system.local WHERE key='local'"; - - private static final String SELECT_SCHEMA_PEERS = - "SELECT peer, rpc_address, schema_version, host_id FROM system.peers"; - private static final String SELECT_SCHEMA_LOCAL = - "SELECT schema_version, host_id FROM system.local WHERE key='local'"; - - private static final VersionNumber _3_11 = VersionNumber.parse("3.11.0"); - - @VisibleForTesting - final AtomicReference connectionRef = new AtomicReference(); - - private final Cluster.Manager cluster; - - private final AtomicReference> reconnectionAttempt = - new AtomicReference>(); - - private volatile boolean isShutdown; - - // set to true initially, if ever fails will be set to false and peers table will be used - // from here on out. - private volatile boolean isPeersV2 = true; - private volatile boolean isCloud = false; - - public ControlConnection(Cluster.Manager manager) { - this.cluster = manager; - } - - // Only for the initial connection. Does not schedule retries if it fails - void connect() throws UnsupportedProtocolVersionException { - if (isShutdown) return; - - List hosts = new ArrayList(cluster.metadata.getContactPoints()); - // shuffle so that multiple clients with the same contact points don't all pick the same control - // host - Collections.shuffle(hosts); - setNewConnection(reconnectInternal(hosts.iterator(), true)); - } - - CloseFuture closeAsync() { - // We don't have to be fancy here. We just set a flag so that we stop trying to reconnect (and - // thus change the - // connection used) and shutdown the current one. - isShutdown = true; - - // Cancel any reconnection attempt in progress - ListenableFuture r = reconnectionAttempt.get(); - if (r != null) r.cancel(false); - - Connection connection = connectionRef.get(); - return connection == null ? CloseFuture.immediateFuture() : connection.closeAsync().force(); - } - - Host connectedHost() { - Connection current = connectionRef.get(); - return (current == null) ? null : cluster.metadata.getHost(current.endPoint); - } - - void triggerReconnect() { - backgroundReconnect(0); - } - - /** @param initialDelayMs if >=0, bypass the schedule and use this for the first call */ - private void backgroundReconnect(long initialDelayMs) { - if (isShutdown) return; - - // Abort if a reconnection is already in progress. This is not thread-safe: two threads might - // race through this and both - // schedule a reconnection; in that case AbstractReconnectionHandler knows how to deal with it - // correctly. - // But this cheap check can help us avoid creating the object unnecessarily. - ListenableFuture reconnection = reconnectionAttempt.get(); - if (reconnection != null && !reconnection.isDone()) return; - - new AbstractReconnectionHandler( - "Control connection", - cluster.reconnectionExecutor, - cluster.reconnectionPolicy().newSchedule(), - reconnectionAttempt, - initialDelayMs) { - @Override - protected Connection tryReconnect() throws ConnectionException { - if (isShutdown) throw new ConnectionException(null, "Control connection was shut down"); - - try { - return reconnectInternal(queryPlan(), false); - } catch (NoHostAvailableException e) { - throw new ConnectionException(null, e.getMessage()); - } catch (UnsupportedProtocolVersionException e) { - // reconnectInternal only propagate those if we've not decided on the protocol version - // yet, - // which should only happen on the initial connection and thus in connect() but never - // here. - throw new AssertionError(); - } - } - - @Override - protected void onReconnection(Connection connection) { - if (isShutdown) { - connection.closeAsync().force(); - return; - } - - setNewConnection(connection); - } - - @Override - protected boolean onConnectionException(ConnectionException e, long nextDelayMs) { - if (isShutdown) return false; - - logger.error( - "[Control connection] Cannot connect to any host, scheduling retry in {} milliseconds", - nextDelayMs); - return true; - } - - @Override - protected boolean onUnknownException(Exception e, long nextDelayMs) { - if (isShutdown) return false; - - logger.error( - String.format( - "[Control connection] Unknown error during reconnection, scheduling retry in %d milliseconds", - nextDelayMs), - e); - return true; - } - }.start(); - } - - private Iterator queryPlan() { - return cluster.loadBalancingPolicy().newQueryPlan(null, Statement.DEFAULT); - } - - private void signalError() { - Connection connection = connectionRef.get(); - if (connection != null) connection.closeAsync().force(); - - // If the error caused the host to go down, onDown might have already triggered a reconnect. - // But backgroundReconnect knows how to deal with that. - backgroundReconnect(0); - } - - private void setNewConnection(Connection newConnection) { - Host.statesLogger.debug("[Control connection] established to {}", newConnection.endPoint); - newConnection.setOwner(this); - Connection old = connectionRef.getAndSet(newConnection); - if (old != null && !old.isClosed()) old.closeAsync().force(); - } - - private Connection reconnectInternal(Iterator iter, boolean isInitialConnection) - throws UnsupportedProtocolVersionException { - - Map errors = null; - - Host host = null; - try { - while (iter.hasNext()) { - host = iter.next(); - if (!host.convictionPolicy.canReconnectNow()) continue; - try { - return tryConnect(host, isInitialConnection); - } catch (ConnectionException e) { - errors = logError(host, e, errors, iter); - if (isInitialConnection) { - // Mark the host down right away so that we don't try it again during the initialization - // process. - // We don't call cluster.triggerOnDown because it does a bunch of other things we don't - // want to do here (notify LBP, etc.) - host.setDown(); - } - } catch (ExecutionException e) { - errors = logError(host, e.getCause(), errors, iter); - } catch (UnsupportedProtocolVersionException e) { - // If it's the very first node we've connected to, rethrow the exception and - // Cluster.init() will handle it. Otherwise, just mark this node in error. - if (isInitialConnection) throw e; - logger.debug("Ignoring host {}: {}", host, e.getMessage()); - errors = logError(host, e, errors, iter); - } catch (ClusterNameMismatchException e) { - logger.debug("Ignoring host {}: {}", host, e.getMessage()); - errors = logError(host, e, errors, iter); - } - } - } catch (InterruptedException e) { - // Sets interrupted status - Thread.currentThread().interrupt(); - - // Indicates that all remaining hosts are skipped due to the interruption - errors = logError(host, new DriverException("Connection thread interrupted"), errors, iter); - while (iter.hasNext()) - errors = - logError( - iter.next(), new DriverException("Connection thread interrupted"), errors, iter); - } - throw new NoHostAvailableException( - errors == null ? Collections.emptyMap() : errors); - } - - private static Map logError( - Host host, Throwable exception, Map errors, Iterator iter) { - if (errors == null) errors = new HashMap(); - - errors.put(host.getEndPoint(), exception); - - if (logger.isDebugEnabled()) { - if (iter.hasNext()) { - logger.debug( - String.format("[Control connection] error on %s connection, trying next host", host), - exception); - } else { - logger.debug( - String.format("[Control connection] error on %s connection, no more host to try", host), - exception); - } - } - return errors; - } - - private Connection tryConnect(Host host, boolean isInitialConnection) - throws ConnectionException, ExecutionException, InterruptedException, - UnsupportedProtocolVersionException, ClusterNameMismatchException { - Connection connection = cluster.connectionFactory.open(host); - String productType = connection.optionsQuery().get(); - if (productType.equals("DATASTAX_APOLLO")) { - isCloud = true; - } - // If no protocol version was specified, set the default as soon as a connection succeeds (it's - // needed to parse UDTs in refreshSchema) - if (cluster.connectionFactory.protocolVersion == null) - cluster.connectionFactory.protocolVersion = ProtocolVersion.NEWEST_SUPPORTED; - - try { - logger.trace("[Control connection] Registering for events"); - List evs = - Arrays.asList( - ProtocolEvent.Type.TOPOLOGY_CHANGE, - ProtocolEvent.Type.STATUS_CHANGE, - ProtocolEvent.Type.SCHEMA_CHANGE); - connection.write(new Requests.Register(evs)); - - // We need to refresh the node list first so we know about the cassandra version of - // the node we're connecting to. - // This will create the token map for the first time, but it will be incomplete - // due to the lack of keyspace information - refreshNodeListAndTokenMap(connection, cluster, isInitialConnection, true); - - // refresh schema will also update the token map again, - // this time with information about keyspaces - logger.debug("[Control connection] Refreshing schema"); - refreshSchema(connection, null, null, null, null, cluster); - - return connection; - } catch (BusyConnectionException e) { - connection.closeAsync().force(); - throw new DriverInternalError("Newly created connection should not be busy"); - } catch (InterruptedException e) { - connection.closeAsync().force(); - throw e; - } catch (ConnectionException e) { - connection.closeAsync().force(); - throw e; - } catch (ExecutionException e) { - connection.closeAsync().force(); - throw e; - } catch (RuntimeException e) { - connection.closeAsync().force(); - throw e; - } - } - - public void refreshSchema( - SchemaElement targetType, String targetKeyspace, String targetName, List signature) - throws InterruptedException { - logger.debug( - "[Control connection] Refreshing schema for {}{}", - targetType == null ? "everything" : targetKeyspace, - (targetType == KEYSPACE) ? "" : "." + targetName + " (" + targetType + ")"); - try { - Connection c = connectionRef.get(); - // At startup, when we add the initial nodes, this will be null, which is ok - if (c == null || c.isClosed()) return; - refreshSchema(c, targetType, targetKeyspace, targetName, signature, cluster); - } catch (ConnectionException e) { - logger.debug( - "[Control connection] Connection error while refreshing schema ({})", e.getMessage()); - signalError(); - } catch (ExecutionException e) { - // If we're being shutdown during schema refresh, this can happen. That's fine so don't scare - // the user. - if (!isShutdown) - logger.error("[Control connection] Unexpected error while refreshing schema", e); - signalError(); - } catch (BusyConnectionException e) { - logger.debug("[Control connection] Connection is busy, reconnecting"); - signalError(); - } - } - - static void refreshSchema( - Connection connection, - SchemaElement targetType, - String targetKeyspace, - String targetName, - List targetSignature, - Cluster.Manager cluster) - throws ConnectionException, BusyConnectionException, ExecutionException, - InterruptedException { - Host host = cluster.metadata.getHost(connection.endPoint); - // Neither host, nor it's version should be null. But instead of dying if there is a race or - // something, we can kind of try to infer - // a Cassandra version from the protocol version (this is not full proof, we can have the - // protocol 1 against C* 2.0+, but it's worth - // a shot, and since we log in this case, it should be relatively easy to debug when if this - // ever fail). - VersionNumber cassandraVersion; - if (host == null || host.getCassandraVersion() == null) { - cassandraVersion = cluster.protocolVersion().minCassandraVersion(); - logger.warn( - "Cannot find Cassandra version for host {} to parse the schema, using {} based on protocol version in use. " - + "If parsing the schema fails, this could be the cause", - connection.endPoint, - cassandraVersion); - } else { - cassandraVersion = host.getCassandraVersion(); - } - SchemaParser schemaParser; - if (host == null) { - schemaParser = SchemaParser.forVersion(cassandraVersion); - } else { - @SuppressWarnings("deprecation") - VersionNumber dseVersion = host.getDseVersion(); - // If using DSE, derive parser from DSE version. - schemaParser = - dseVersion == null - ? SchemaParser.forVersion(cassandraVersion) - : SchemaParser.forDseVersion(dseVersion); - if (dseVersion != null && dseVersion.getMajor() == 6 && dseVersion.getMinor() < 8) { - // DSE 6.0 and 6.7 report C* 4.0, but consider it C* 3.11 for schema parsing purposes - cassandraVersion = _3_11; - } - } - - schemaParser.refresh( - cluster.getCluster(), - targetType, - targetKeyspace, - targetName, - targetSignature, - connection, - cassandraVersion); - } - - void refreshNodeListAndTokenMap() { - Connection c = connectionRef.get(); - // At startup, when we add the initial nodes, this will be null, which is ok - if (c == null || c.isClosed()) return; - - try { - refreshNodeListAndTokenMap(c, cluster, false, true); - } catch (ConnectionException e) { - logger.debug( - "[Control connection] Connection error while refreshing node list and token map ({})", - e.getMessage()); - signalError(); - } catch (ExecutionException e) { - // If we're being shutdown during refresh, this can happen. That's fine so don't scare the - // user. - if (!isShutdown) - logger.error( - "[Control connection] Unexpected error while refreshing node list and token map", e); - signalError(); - } catch (BusyConnectionException e) { - logger.debug("[Control connection] Connection is busy, reconnecting"); - signalError(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.debug( - "[Control connection] Interrupted while refreshing node list and token map, skipping it."); - } - } - - private static EndPoint endPointForPeerHost( - Row peersRow, EndPoint connectedEndPoint, Cluster.Manager cluster) { - EndPoint endPoint = cluster.configuration.getPolicies().getEndPointFactory().create(peersRow); - if (connectedEndPoint.equals(endPoint)) { - // Some DSE versions were inserting a line for the local node in peers (with mostly null - // values). This has been fixed, but if we detect that's the case, ignore it as it's not - // really a big deal. - logger.debug( - "System.peers on node {} has a line for itself. " - + "This is not normal but is a known problem of some DSE versions. " - + "Ignoring the entry.", - connectedEndPoint); - return null; - } - return endPoint; - } - - private Row fetchNodeInfo(Host host, Connection c) - throws ConnectionException, BusyConnectionException, ExecutionException, - InterruptedException { - boolean isConnectedHost = c.endPoint.equals(host.getEndPoint()); - if (isConnectedHost || host.getBroadcastSocketAddress() != null) { - String query; - if (isConnectedHost) { - query = SELECT_LOCAL; - } else { - InetSocketAddress broadcastAddress = host.getBroadcastSocketAddress(); - query = - isPeersV2 - ? SELECT_PEERS_V2 - + " WHERE peer='" - + broadcastAddress.getAddress().getHostAddress() - + "' AND peer_port=" - + broadcastAddress.getPort() - : SELECT_PEERS - + " WHERE peer='" - + broadcastAddress.getAddress().getHostAddress() - + "'"; - } - DefaultResultSetFuture future = - new DefaultResultSetFuture(null, cluster.protocolVersion(), new Requests.Query(query)); - c.write(future); - Row row = future.get().one(); - if (row != null) { - return row; - } else { - InetSocketAddress address = host.getBroadcastSocketAddress(); - // Don't include full address if port is 0. - String addressToUse = - address.getPort() != 0 ? address.toString() : address.getAddress().toString(); - logger.debug( - "Could not find peer with broadcast address {}, " - + "falling back to a full system.peers scan to fetch info for {} " - + "(this can happen if the broadcast address changed)", - addressToUse, - host); - } - } - - // We have to fetch the whole peers table and find the host we're looking for - ListenableFuture future = selectPeersFuture(c); - for (Row row : future.get()) { - UUID rowId = row.getUUID("host_id"); - if (host.getHostId().equals(rowId)) { - return row; - } - } - return null; - } - - /** @return whether we have enough information to bring the node back up */ - boolean refreshNodeInfo(Host host) { - - Connection c = connectionRef.get(); - // At startup, when we add the initial nodes, this will be null, which is ok - if (c == null || c.isClosed()) return true; - - logger.debug("[Control connection] Refreshing node info on {}", host); - try { - Row row = fetchNodeInfo(host, c); - if (row == null) { - if (c.isDefunct()) { - logger.debug("Control connection is down, could not refresh node info"); - // Keep going with what we currently know about the node, otherwise we will ignore all - // nodes - // until the control connection is back up (which leads to a catch-22 if there is only - // one) - return true; - } else { - logger.warn( - "No row found for host {} in {}'s peers system table. {} will be ignored.", - host.getEndPoint(), - c.endPoint, - host.getEndPoint()); - return false; - } - // Ignore hosts with a null rpc_address, as this is most likely a phantom row in - // system.peers (JAVA-428). - // Don't test this for the control host since we're already connected to it anyway, and we - // read the info from system.local - // which didn't have an rpc_address column (JAVA-546) until CASSANDRA-9436 - } else if (!c.endPoint.equals(host.getEndPoint()) && !isValidPeer(row, true)) { - return false; - } - - updateInfo(host, row, cluster, false); - return true; - - } catch (ConnectionException e) { - logger.debug( - "[Control connection] Connection error while refreshing node info ({})", e.getMessage()); - signalError(); - } catch (ExecutionException e) { - // If we're being shutdown during refresh, this can happen. That's fine so don't scare the - // user. - if (!isShutdown) - logger.debug("[Control connection] Unexpected error while refreshing node info", e); - signalError(); - } catch (BusyConnectionException e) { - logger.debug("[Control connection] Connection is busy, reconnecting"); - signalError(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - logger.debug("[Control connection] Interrupted while refreshing node info, skipping it."); - } catch (Exception e) { - logger.debug("[Control connection] Unexpected error while refreshing node info", e); - signalError(); - } - // If we got an exception, always return true. Otherwise a faulty control connection would cause - // reconnected hosts to be ignored permanently. - return true; - } - - // row can come either from the 'local' table or the 'peers' one - private static void updateInfo( - Host host, Row row, Cluster.Manager cluster, boolean isInitialConnection) { - if (!row.isNull("data_center") || !row.isNull("rack")) - updateLocationInfo( - host, row.getString("data_center"), row.getString("rack"), isInitialConnection, cluster); - - String version = row.getString("release_version"); - host.setVersion(version); - - // Before CASSANDRA-9436 local row did not contain any info about the host addresses. - // After CASSANDRA-9436 (2.0.16, 2.1.6, 2.2.0 rc1) local row contains two new columns: - // - broadcast_address - // - rpc_address - // After CASSANDRA-9603 (2.0.17, 2.1.8, 2.2.0 rc2) local row contains one more column: - // - listen_address - // After CASSANDRA-7544 (4.0) local row also contains: - // - broadcast_port - // - listen_port - - InetSocketAddress broadcastRpcAddress = null; - if (row.getColumnDefinitions().contains("native_address")) { - InetAddress nativeAddress = row.getInet("native_address"); - int nativePort = row.getInt("native_port"); - broadcastRpcAddress = new InetSocketAddress(nativeAddress, nativePort); - } else if (row.getColumnDefinitions().contains("native_transport_address")) { - // DSE 6.8 introduced native_transport_address and native_transport_port for the - // listen address. Also included is native_transport_port_ssl (in case users - // want to setup a different port for SSL and non-SSL conns). - InetAddress nativeAddress = row.getInet("native_transport_address"); - int nativePort = row.getInt("native_transport_port"); - if (cluster.getCluster().getConfiguration().getProtocolOptions().getSSLOptions() != null - && !row.isNull("native_transport_port_ssl")) { - nativePort = row.getInt("native_transport_port_ssl"); - } - broadcastRpcAddress = new InetSocketAddress(nativeAddress, nativePort); - } else if (row.getColumnDefinitions().contains("rpc_address")) { - InetAddress rpcAddress = row.getInet("rpc_address"); - broadcastRpcAddress = new InetSocketAddress(rpcAddress, cluster.connectionFactory.getPort()); - } - // Before CASSANDRA-9436, system.local doesn't have rpc_address, so this might be null. It's not - // a big deal because we only use this for server events, and the control node doesn't receive - // events for itself. - host.setBroadcastRpcAddress(broadcastRpcAddress); - - InetSocketAddress broadcastSocketAddress = null; - if (row.getColumnDefinitions().contains("peer")) { // system.peers - int broadcastPort = - row.getColumnDefinitions().contains("peer_port") ? row.getInt("peer_port") : 0; - broadcastSocketAddress = new InetSocketAddress(row.getInet("peer"), broadcastPort); - } else if (row.getColumnDefinitions().contains("broadcast_address")) { // system.local - int broadcastPort = - row.getColumnDefinitions().contains("broadcast_port") ? row.getInt("broadcast_port") : 0; - broadcastSocketAddress = - new InetSocketAddress(row.getInet("broadcast_address"), broadcastPort); - } - host.setBroadcastSocketAddress(broadcastSocketAddress); - - // in system.local only for C* versions >= 2.0.17, 2.1.8, 2.2.0 rc2, - // not yet in system.peers as of C* 3.2 - InetSocketAddress listenAddress = null; - if (row.getColumnDefinitions().contains("listen_address")) { - int listenPort = - row.getColumnDefinitions().contains("listen_port") ? row.getInt("listen_port") : 0; - listenAddress = new InetSocketAddress(row.getInet("listen_address"), listenPort); - } - host.setListenSocketAddress(listenAddress); - - if (row.getColumnDefinitions().contains("workload")) { - String dseWorkload = row.getString("workload"); - host.setDseWorkload(dseWorkload); - } - if (row.getColumnDefinitions().contains("graph")) { - boolean isDseGraph = row.getBool("graph"); - host.setDseGraphEnabled(isDseGraph); - } - if (row.getColumnDefinitions().contains("dse_version")) { - String dseVersion = row.getString("dse_version"); - host.setDseVersion(dseVersion); - } - host.setHostId(row.getUUID("host_id")); - host.setSchemaVersion(row.getUUID("schema_version")); - } - - private static void updateLocationInfo( - Host host, - String datacenter, - String rack, - boolean isInitialConnection, - Cluster.Manager cluster) { - if (MoreObjects.equal(host.getDatacenter(), datacenter) - && MoreObjects.equal(host.getRack(), rack)) return; - - // If the dc/rack information changes for an existing node, we need to update the load balancing - // policy. - // For that, we remove and re-add the node against the policy. Not the most elegant, and assumes - // that the policy will update correctly, but in practice this should work. - if (!isInitialConnection) cluster.loadBalancingPolicy().onRemove(host); - host.setLocationInfo(datacenter, rack); - if (!isInitialConnection) cluster.loadBalancingPolicy().onAdd(host); - } - - /** - * Resolves peering information by doing the following: - * - *

    - *
  1. if isPeersV2 is true, query the system.peers_v2 table, - * otherwise query system.peers. - *
  2. if system.peers_v2 query fails, set isPeersV2 to false and call - * selectPeersFuture again. - *
- * - * @param connection connection to send request on. - * @return result of peers query. - */ - private ListenableFuture selectPeersFuture(final Connection connection) { - if (isPeersV2) { - DefaultResultSetFuture peersV2Future = - new DefaultResultSetFuture( - null, cluster.protocolVersion(), new Requests.Query(SELECT_PEERS_V2)); - connection.write(peersV2Future); - final SettableFuture peersFuture = SettableFuture.create(); - // if peers v2 query fails, query peers table instead. - GuavaCompatibility.INSTANCE.addCallback( - peersV2Future, - new FutureCallback() { - - @Override - public void onSuccess(ResultSet result) { - peersFuture.set(result); - } - - @Override - public void onFailure(Throwable t) { - // Downgrade to system.peers if we get an invalid query error as this indicates the - // peers_v2 table does not exist. - // Also downgrade on server error with a specific error message (DSE 6.0.0 to 6.0.2 - // with search enabled. - if (t instanceof InvalidQueryException - || (t instanceof ServerError - && t.getMessage().contains("Unknown keyspace/cf pair (system.peers_v2)"))) { - isPeersV2 = false; - MoreFutures.propagateFuture(peersFuture, selectPeersFuture(connection)); - } else { - peersFuture.setException(t); - } - } - }); - return peersFuture; - } else { - DefaultResultSetFuture peersFuture = - new DefaultResultSetFuture( - null, cluster.protocolVersion(), new Requests.Query(SELECT_PEERS)); - connection.write(peersFuture); - return peersFuture; - } - } - - private void refreshNodeListAndTokenMap( - final Connection connection, - final Cluster.Manager cluster, - boolean isInitialConnection, - boolean logInvalidPeers) - throws ConnectionException, BusyConnectionException, ExecutionException, - InterruptedException { - logger.debug("[Control connection] Refreshing node list and token map"); - - boolean metadataEnabled = cluster.configuration.getQueryOptions().isMetadataEnabled(); - - // Make sure we're up to date on nodes and tokens - - DefaultResultSetFuture localFuture = - new DefaultResultSetFuture( - null, cluster.protocolVersion(), new Requests.Query(SELECT_LOCAL)); - ListenableFuture peersFuture = selectPeersFuture(connection); - connection.write(localFuture); - - String partitioner = null; - Token.Factory factory = null; - Map> tokenMap = new HashMap>(); - - // Update cluster name, DC and rack for the one node we are connected to - Row localRow = localFuture.get().one(); - if (localRow == null) { - throw new IllegalStateException( - String.format( - "system.local is empty on %s, this should not happen", connection.endPoint)); - } - String clusterName = localRow.getString("cluster_name"); - if (clusterName != null) cluster.metadata.clusterName = clusterName; - - partitioner = localRow.getString("partitioner"); - if (partitioner != null) { - cluster.metadata.partitioner = partitioner; - factory = Token.getFactory(partitioner); - } - - // During init, metadata.allHosts is still empty, the contact points are in - // metadata.contactPoints. We need to copy them over, but we can only do it after having - // called updateInfo, because we need to know the host id. - // This is the same for peer hosts (see further down). - Host controlHost = - isInitialConnection - ? cluster.metadata.getContactPoint(connection.endPoint) - : cluster.metadata.getHost(connection.endPoint); - // In theory host can't be null. However there is no point in risking a NPE in case we - // have a race between a node removal and this. - if (controlHost == null) { - logger.debug( - "Host in local system table ({}) unknown to us (ok if said host just got removed)", - connection.endPoint); - } else { - updateInfo(controlHost, localRow, cluster, isInitialConnection); - if (metadataEnabled && factory != null) { - Set tokensStr = localRow.getSet("tokens", String.class); - if (!tokensStr.isEmpty()) { - Set tokens = toTokens(factory, tokensStr); - tokenMap.put(controlHost, tokens); - } - } - if (isInitialConnection) { - cluster.metadata.addIfAbsent(controlHost); - } - } - - List foundHosts = new ArrayList(); - List dcs = new ArrayList(); - List racks = new ArrayList(); - List cassandraVersions = new ArrayList(); - List broadcastRpcAddresses = new ArrayList(); - List broadcastAddresses = new ArrayList(); - List listenAddresses = new ArrayList(); - List> allTokens = new ArrayList>(); - List dseVersions = new ArrayList(); - List dseGraphEnabled = new ArrayList(); - List dseWorkloads = new ArrayList(); - List hostIds = new ArrayList(); - List schemaVersions = new ArrayList(); - - for (Row row : peersFuture.get()) { - if (!isValidPeer(row, logInvalidPeers)) continue; - - EndPoint endPoint = endPointForPeerHost(row, connection.endPoint, cluster); - if (endPoint == null) { - continue; - } - foundHosts.add(endPoint); - dcs.add(row.getString("data_center")); - racks.add(row.getString("rack")); - cassandraVersions.add(row.getString("release_version")); - - InetSocketAddress broadcastRpcAddress; - if (row.getColumnDefinitions().contains("native_address")) { - InetAddress nativeAddress = row.getInet("native_address"); - int nativePort = row.getInt("native_port"); - broadcastRpcAddress = new InetSocketAddress(nativeAddress, nativePort); - } else if (row.getColumnDefinitions().contains("native_transport_address")) { - InetAddress nativeAddress = row.getInet("native_transport_address"); - int nativePort = row.getInt("native_transport_port"); - if (cluster.getCluster().getConfiguration().getProtocolOptions().getSSLOptions() != null - && !row.isNull("native_transport_port_ssl")) { - nativePort = row.getInt("native_transport_port_ssl"); - } - broadcastRpcAddress = new InetSocketAddress(nativeAddress, nativePort); - } else { - InetAddress rpcAddress = row.getInet("rpc_address"); - broadcastRpcAddress = - new InetSocketAddress(rpcAddress, cluster.connectionFactory.getPort()); - } - broadcastRpcAddresses.add(broadcastRpcAddress); - - int broadcastPort = - row.getColumnDefinitions().contains("peer_port") ? row.getInt("peer_port") : 0; - InetSocketAddress broadcastAddress = - new InetSocketAddress(row.getInet("peer"), broadcastPort); - - broadcastAddresses.add(broadcastAddress); - if (metadataEnabled && factory != null) { - Set tokensStr = row.getSet("tokens", String.class); - Set tokens = null; - if (!tokensStr.isEmpty()) { - tokens = toTokens(factory, tokensStr); - } - allTokens.add(tokens); - } - - if (row.getColumnDefinitions().contains("listen_address") && !row.isNull("listen_address")) { - int listenPort = - row.getColumnDefinitions().contains("listen_port") ? row.getInt("listen_port") : 0; - InetSocketAddress listenAddress = - new InetSocketAddress(row.getInet("listen_address"), listenPort); - listenAddresses.add(listenAddress); - } else { - listenAddresses.add(null); - } - String dseWorkload = - row.getColumnDefinitions().contains("workload") ? row.getString("workload") : null; - dseWorkloads.add(dseWorkload); - Boolean isDseGraph = - row.getColumnDefinitions().contains("graph") ? row.getBool("graph") : null; - dseGraphEnabled.add(isDseGraph); - String dseVersion = - row.getColumnDefinitions().contains("dse_version") ? row.getString("dse_version") : null; - dseVersions.add(dseVersion); - hostIds.add(row.getUUID("host_id")); - schemaVersions.add(row.getUUID("schema_version")); - } - - for (int i = 0; i < foundHosts.size(); i++) { - Host peerHost = - isInitialConnection - ? cluster.metadata.getContactPoint(foundHosts.get(i)) - : cluster.metadata.getHost(foundHosts.get(i)); - boolean isNew = false; - if (peerHost == null) { - // We don't know that node, create the Host object but wait until we've set the known - // info before signaling the addition. - Host newHost = cluster.metadata.newHost(foundHosts.get(i)); - newHost.setHostId(hostIds.get(i)); // we need an id to add to the metadata - Host previous = cluster.metadata.addIfAbsent(newHost); - if (previous == null) { - peerHost = newHost; - isNew = true; - } else { - peerHost = previous; - isNew = false; - } - } - if (dcs.get(i) != null || racks.get(i) != null) - updateLocationInfo(peerHost, dcs.get(i), racks.get(i), isInitialConnection, cluster); - if (cassandraVersions.get(i) != null) peerHost.setVersion(cassandraVersions.get(i)); - if (broadcastRpcAddresses.get(i) != null) - peerHost.setBroadcastRpcAddress(broadcastRpcAddresses.get(i)); - if (broadcastAddresses.get(i) != null) - peerHost.setBroadcastSocketAddress(broadcastAddresses.get(i)); - if (listenAddresses.get(i) != null) peerHost.setListenSocketAddress(listenAddresses.get(i)); - - if (dseVersions.get(i) != null) peerHost.setDseVersion(dseVersions.get(i)); - if (dseWorkloads.get(i) != null) peerHost.setDseWorkload(dseWorkloads.get(i)); - if (dseGraphEnabled.get(i) != null) peerHost.setDseGraphEnabled(dseGraphEnabled.get(i)); - peerHost.setHostId(hostIds.get(i)); - if (schemaVersions.get(i) != null) { - peerHost.setSchemaVersion(schemaVersions.get(i)); - } - - if (metadataEnabled && factory != null && allTokens.get(i) != null) - tokenMap.put(peerHost, allTokens.get(i)); - - if (!isNew && isInitialConnection) { - // If we're at init and the node already existed, it means it was a contact point, so we - // need to copy it over to the regular host list - cluster.metadata.addIfAbsent(peerHost); - } - if (isNew && !isInitialConnection) { - cluster.triggerOnAdd(peerHost); - } - } - - // Removes all those that seem to have been removed (since we lost the control connection) - Set foundHostsSet = new HashSet(foundHosts); - for (Host host : cluster.metadata.allHosts()) - if (!host.getEndPoint().equals(connection.endPoint) - && !foundHostsSet.contains(host.getEndPoint())) - cluster.removeHost(host, isInitialConnection); - - if (metadataEnabled && factory != null && !tokenMap.isEmpty()) - cluster.metadata.rebuildTokenMap(factory, tokenMap); - } - - private static Set toTokens(Token.Factory factory, Set tokensStr) { - Set tokens = new LinkedHashSet(tokensStr.size()); - for (String tokenStr : tokensStr) { - tokens.add(factory.fromString(tokenStr)); - } - return tokens; - } - - private boolean isValidPeer(Row peerRow, boolean logIfInvalid) { - boolean isValid = - peerRow.getColumnDefinitions().contains("host_id") && !peerRow.isNull("host_id"); - - if (isPeersV2) { - isValid &= - peerRow.getColumnDefinitions().contains("native_address") - && peerRow.getColumnDefinitions().contains("native_port") - && !peerRow.isNull("native_address") - && !peerRow.isNull("native_port"); - } else { - isValid &= - (peerRow.getColumnDefinitions().contains("rpc_address") && !peerRow.isNull("rpc_address")) - || (peerRow.getColumnDefinitions().contains("native_transport_address") - && peerRow.getColumnDefinitions().contains("native_transport_port") - && !peerRow.isNull("native_transport_address") - && !peerRow.isNull("native_transport_port")); - } - - if (EXTENDED_PEER_CHECK) { - isValid &= - peerRow.getColumnDefinitions().contains("data_center") - && !peerRow.isNull("data_center") - && peerRow.getColumnDefinitions().contains("rack") - && !peerRow.isNull("rack") - && peerRow.getColumnDefinitions().contains("tokens") - && !peerRow.isNull("tokens"); - } - if (!isValid && logIfInvalid) - logger.warn( - "Found invalid row in system.peers: {}. " - + "This is likely a gossip or snitch issue, this host will be ignored.", - formatInvalidPeer(peerRow)); - return isValid; - } - - // Custom formatting to avoid spamming the logs if 'tokens' is present and contains a gazillion - // tokens - private String formatInvalidPeer(Row peerRow) { - StringBuilder sb = new StringBuilder("[peer=" + peerRow.getInet("peer")); - if (isPeersV2) { - formatMissingOrNullColumn(peerRow, "native_address", sb); - formatMissingOrNullColumn(peerRow, "native_port", sb); - } else { - formatMissingOrNullColumn(peerRow, "native_transport_address", sb); - formatMissingOrNullColumn(peerRow, "native_transport_port", sb); - formatMissingOrNullColumn(peerRow, "native_transport_port_ssl", sb); - formatMissingOrNullColumn(peerRow, "rpc_address", sb); - } - if (EXTENDED_PEER_CHECK) { - formatMissingOrNullColumn(peerRow, "host_id", sb); - formatMissingOrNullColumn(peerRow, "data_center", sb); - formatMissingOrNullColumn(peerRow, "rack", sb); - formatMissingOrNullColumn(peerRow, "tokens", sb); - } - sb.append("]"); - return sb.toString(); - } - - private static void formatMissingOrNullColumn(Row peerRow, String columnName, StringBuilder sb) { - if (!peerRow.getColumnDefinitions().contains(columnName)) - sb.append(", missing ").append(columnName); - else if (peerRow.isNull(columnName)) sb.append(", ").append(columnName).append("=null"); - } - - static boolean waitForSchemaAgreement(Connection connection, Cluster.Manager cluster) - throws ConnectionException, BusyConnectionException, ExecutionException, - InterruptedException { - long start = System.nanoTime(); - long elapsed = 0; - int maxSchemaAgreementWaitSeconds = - cluster.configuration.getProtocolOptions().getMaxSchemaAgreementWaitSeconds(); - while (elapsed < maxSchemaAgreementWaitSeconds * 1000) { - - if (checkSchemaAgreement(connection, cluster)) return true; - - // let's not flood the node too much - Thread.sleep(200); - - elapsed = Cluster.timeSince(start, TimeUnit.MILLISECONDS); - } - - return false; - } - - private static boolean checkSchemaAgreement(Connection connection, Cluster.Manager cluster) - throws InterruptedException, ExecutionException { - DefaultResultSetFuture peersFuture = - new DefaultResultSetFuture( - null, cluster.protocolVersion(), new Requests.Query(SELECT_SCHEMA_PEERS)); - DefaultResultSetFuture localFuture = - new DefaultResultSetFuture( - null, cluster.protocolVersion(), new Requests.Query(SELECT_SCHEMA_LOCAL)); - connection.write(peersFuture); - connection.write(localFuture); - - Set versions = new HashSet(); - - Row localRow = localFuture.get().one(); - if (localRow != null && !localRow.isNull("schema_version")) - versions.add(localRow.getUUID("schema_version")); - - for (Row row : peersFuture.get()) { - - UUID hostId = row.getUUID("host_id"); - if (row.isNull("schema_version")) continue; - - Host peer = cluster.metadata.getHost(hostId); - if (peer != null && peer.isUp()) versions.add(row.getUUID("schema_version")); - } - logger.debug("Checking for schema agreement: versions are {}", versions); - return versions.size() <= 1; - } - - boolean checkSchemaAgreement() - throws ConnectionException, BusyConnectionException, InterruptedException, - ExecutionException { - Connection connection = connectionRef.get(); - return connection != null - && !connection.isClosed() - && checkSchemaAgreement(connection, cluster); - } - - boolean isOpen() { - Connection c = connectionRef.get(); - return c != null && !c.isClosed(); - } - - boolean isCloud() { - return isCloud; - } - - public void onUp(Host host) {} - - public void onAdd(Host host) {} - - public void onDown(Host host) { - onHostGone(host); - } - - public void onRemove(Host host) { - onHostGone(host); - } - - private void onHostGone(Host host) { - Connection current = connectionRef.get(); - - if (current != null && current.endPoint.equals(host.getEndPoint())) { - logger.debug( - "[Control connection] {} is down/removed and it was the control host, triggering reconnect", - current.endPoint); - if (!current.isClosed()) current.closeAsync().force(); - backgroundReconnect(0); - } - } - - @Override - public void onConnectionDefunct(Connection connection) { - if (connection == connectionRef.get()) backgroundReconnect(0); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ConvictionPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/ConvictionPolicy.java deleted file mode 100644 index 85907629906..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ConvictionPolicy.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.NANOSECONDS; - -import com.datastax.driver.core.policies.ReconnectionPolicy; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * The policy with which to decide whether a host should be considered down. - * - *

TODO: this class is fully abstract (rather than an interface) because I'm not sure it's worth - * exposing (and if we do expose it, we need to expose ConnectionException). Maybe just exposing say - * a threshold of error before convicting a node is enough. - */ -abstract class ConvictionPolicy { - - /** - * Called when new connections to the host are about to be created. - * - * @param count the number of connections - */ - abstract void signalConnectionsOpening(int count); - - /** Called when a connection closed normally. */ - abstract void signalConnectionClosed(Connection connection); - - /** - * Called when a connection error occurs on a connection to the host this policy applies to. - * - * @return whether the host should be considered down. - */ - abstract boolean signalConnectionFailure(Connection connection, boolean decrement); - - abstract boolean canReconnectNow(); - - abstract boolean hasActiveConnections(); - - /** Simple factory interface to allow creating {@link ConvictionPolicy} instances. */ - interface Factory { - - /** - * Creates a new ConvictionPolicy instance for {@code host}. - * - * @param host the host this policy applies to - * @return the newly created {@link ConvictionPolicy} instance. - */ - ConvictionPolicy create(Host host, ReconnectionPolicy reconnectionPolicy); - } - - static class DefaultConvictionPolicy extends ConvictionPolicy { - private final Host host; - private final ReconnectionPolicy reconnectionPolicy; - private final AtomicInteger openConnections = new AtomicInteger(); - - private volatile long nextReconnectionTime = Long.MIN_VALUE; - private ReconnectionPolicy.ReconnectionSchedule reconnectionSchedule; - - private DefaultConvictionPolicy(Host host, ReconnectionPolicy reconnectionPolicy) { - this.host = host; - this.reconnectionPolicy = reconnectionPolicy; - } - - @Override - void signalConnectionsOpening(int count) { - int newTotal = openConnections.addAndGet(count); - Host.statesLogger.debug( - "[{}] preparing to open {} new connections, total = {}", host, count, newTotal); - resetReconnectionTime(); - } - - @Override - void signalConnectionClosed(Connection connection) { - int remaining = openConnections.decrementAndGet(); - assert remaining >= 0; - Host.statesLogger.debug("[{}] {} closed, remaining = {}", host, connection, remaining); - } - - @Override - boolean signalConnectionFailure(Connection connection, boolean decrement) { - int remaining; - if (decrement) { - if (host.state != Host.State.DOWN) updateReconnectionTime(); - - remaining = openConnections.decrementAndGet(); - assert remaining >= 0; - Host.statesLogger.debug("[{}] {} failed, remaining = {}", host, connection, remaining); - } else { - remaining = openConnections.get(); - } - return remaining == 0; - } - - private synchronized void updateReconnectionTime() { - long now = System.nanoTime(); - if (nextReconnectionTime > now) - // Someone else updated the time before us - return; - - if (reconnectionSchedule == null) reconnectionSchedule = reconnectionPolicy.newSchedule(); - - long nextDelayMs = reconnectionSchedule.nextDelayMs(); - Host.statesLogger.debug( - "[{}] preventing new connections for the next {} ms", host, nextDelayMs); - nextReconnectionTime = now + NANOSECONDS.convert(nextDelayMs, MILLISECONDS); - } - - private synchronized void resetReconnectionTime() { - reconnectionSchedule = null; - nextReconnectionTime = Long.MIN_VALUE; - } - - @Override - boolean canReconnectNow() { - boolean canReconnectNow = - nextReconnectionTime == Long.MIN_VALUE || System.nanoTime() >= nextReconnectionTime; - Host.statesLogger.trace("canReconnectNow={}", canReconnectNow); - return canReconnectNow; - } - - @Override - boolean hasActiveConnections() { - return openConnections.get() > 0; - } - - static class Factory implements ConvictionPolicy.Factory { - - @Override - public ConvictionPolicy create(Host host, ReconnectionPolicy reconnectionPolicy) { - return new DefaultConvictionPolicy(host, reconnectionPolicy); - } - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Crc.java b/driver-core/src/main/java/com/datastax/driver/core/Crc.java deleted file mode 100644 index 2abcfbfa6c0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Crc.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.buffer.ByteBuf; -import io.netty.util.concurrent.FastThreadLocal; -import java.nio.ByteBuffer; -import java.util.zip.CRC32; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Copied and adapted from the server-side version. */ -class Crc { - - private static final Logger logger = LoggerFactory.getLogger(Crc.class); - - private static final FastThreadLocal crc32 = - new FastThreadLocal() { - @Override - protected CRC32 initialValue() { - return new CRC32(); - } - }; - - private static final byte[] initialBytes = - new byte[] {(byte) 0xFA, (byte) 0x2D, (byte) 0x55, (byte) 0xCA}; - - private static final CrcUpdater CRC_UPDATER = selectCrcUpdater(); - - static int computeCrc32(ByteBuf buffer) { - CRC32 crc = newCrc32(); - CRC_UPDATER.update(crc, buffer); - return (int) crc.getValue(); - } - - private static CRC32 newCrc32() { - CRC32 crc = crc32.get(); - crc.reset(); - crc.update(initialBytes); - return crc; - } - - private static final int CRC24_INIT = 0x875060; - /** - * Polynomial chosen from https://users.ece.cmu.edu/~koopman/crc/index.html, by Philip Koopman - * - *

This webpage claims a copyright to Philip Koopman, which he licenses under the Creative - * Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0) - * - *

It is unclear if this copyright can extend to a 'fact' such as this specific number, - * particularly as we do not use Koopman's notation to represent the polynomial, but we anyway - * attribute his work and link the terms of his license since they are not incompatible with our - * usage and we greatly appreciate his work. - * - *

This polynomial provides hamming distance of 8 for messages up to length 105 bits; we only - * support 8-64 bits at present, with an expected range of 40-48. - */ - private static final int CRC24_POLY = 0x1974F0B; - - /** - * NOTE: the order of bytes must reach the wire in the same order the CRC is computed, with the - * CRC immediately following in a trailer. Since we read in least significant byte order, if you - * write to a buffer using putInt or putLong, the byte order will be reversed and you will lose - * the guarantee of protection from burst corruptions of 24 bits in length. - * - *

Make sure either to write byte-by-byte to the wire, or to use Integer/Long.reverseBytes if - * you write to a BIG_ENDIAN buffer. - * - *

See http://users.ece.cmu.edu/~koopman/pubs/ray06_crcalgorithms.pdf - * - *

Complain to the ethernet spec writers, for having inverse bit to byte significance order. - * - *

Note we use the most naive algorithm here. We support at most 8 bytes, and typically supply - * 5 or fewer, so any efficiency of a table approach is swallowed by the time to hit L3, even for - * a tiny (4bit) table. - * - * @param bytes an up to 8-byte register containing bytes to compute the CRC over the bytes AND - * bits will be read least-significant to most significant. - * @param len the number of bytes, greater than 0 and fewer than 9, to be read from bytes - * @return the least-significant bit AND byte order crc24 using the CRC24_POLY polynomial - */ - static int computeCrc24(long bytes, int len) { - int crc = CRC24_INIT; - while (len-- > 0) { - crc ^= (bytes & 0xff) << 16; - bytes >>= 8; - - for (int i = 0; i < 8; i++) { - crc <<= 1; - if ((crc & 0x1000000) != 0) crc ^= CRC24_POLY; - } - } - return crc; - } - - private static CrcUpdater selectCrcUpdater() { - try { - CRC32.class.getDeclaredMethod("update", ByteBuffer.class); - return new Java8CrcUpdater(); - } catch (Exception e) { - logger.warn( - "It looks like you are running Java 7 or below. " - + "CRC checks (used in protocol {} and above) will require a memory copy, which can " - + "negatively impact performance. Consider using a more modern VM.", - ProtocolVersion.V5, - e); - return new Java6CrcUpdater(); - } - } - - private interface CrcUpdater { - void update(CRC32 crc, ByteBuf buffer); - } - - private static class Java6CrcUpdater implements CrcUpdater { - @Override - public void update(CRC32 crc, ByteBuf buffer) { - if (buffer.hasArray()) { - crc.update(buffer.array(), buffer.arrayOffset(), buffer.readableBytes()); - } else { - byte[] bytes = new byte[buffer.readableBytes()]; - buffer.getBytes(buffer.readerIndex(), bytes); - crc.update(bytes); - } - } - } - - @IgnoreJDK6Requirement - private static class Java8CrcUpdater implements CrcUpdater { - @Override - public void update(CRC32 crc, ByteBuf buffer) { - crc.update(buffer.internalNioBuffer(buffer.readerIndex(), buffer.readableBytes())); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/DataType.java b/driver-core/src/main/java/com/datastax/driver/core/DataType.java deleted file mode 100644 index 352716e2cdc..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/DataType.java +++ /dev/null @@ -1,739 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import io.netty.buffer.ByteBuf; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** Data types supported by cassandra. */ -public abstract class DataType { - - /** The CQL type name. */ - public enum Name { - CUSTOM(0), - ASCII(1), - BIGINT(2), - BLOB(3), - BOOLEAN(4), - COUNTER(5), - DECIMAL(6), - DOUBLE(7), - FLOAT(8), - INT(9), - TEXT(10) { - @Override - public boolean isCompatibleWith(Name that) { - return this == that || that == VARCHAR; - } - }, - TIMESTAMP(11), - UUID(12), - VARCHAR(13) { - @Override - public boolean isCompatibleWith(Name that) { - return this == that || that == TEXT; - } - }, - VARINT(14), - TIMEUUID(15), - INET(16), - DATE(17, ProtocolVersion.V4), - TIME(18, ProtocolVersion.V4), - SMALLINT(19, ProtocolVersion.V4), - TINYINT(20, ProtocolVersion.V4), - DURATION(21, ProtocolVersion.V5), - LIST(32), - MAP(33), - SET(34), - UDT(48, ProtocolVersion.V3), - TUPLE(49, ProtocolVersion.V3); - - final int protocolId; - - final ProtocolVersion minProtocolVersion; - - private static final Name[] nameToIds; - - static { - int maxCode = -1; - for (Name name : Name.values()) maxCode = Math.max(maxCode, name.protocolId); - nameToIds = new Name[maxCode + 1]; - for (Name name : Name.values()) { - if (nameToIds[name.protocolId] != null) throw new IllegalStateException("Duplicate Id"); - nameToIds[name.protocolId] = name; - } - } - - private Name(int protocolId) { - this(protocolId, ProtocolVersion.V1); - } - - private Name(int protocolId, ProtocolVersion minProtocolVersion) { - this.protocolId = protocolId; - this.minProtocolVersion = minProtocolVersion; - } - - static Name fromProtocolId(int id) { - Name name = nameToIds[id]; - if (name == null) throw new DriverInternalError("Unknown data type protocol id: " + id); - return name; - } - - /** - * Return {@code true} if the provided Name is equal to this one, or if they are aliases for - * each other, and {@code false} otherwise. - * - * @param that the Name to compare with the current one. - * @return {@code true} if the provided Name is equal to this one, or if they are aliases for - * each other, and {@code false} otherwise. - */ - public boolean isCompatibleWith(Name that) { - return this == that; - } - - @Override - public String toString() { - return super.toString().toLowerCase(); - } - } - - private static final Map primitiveTypeMap = - new EnumMap(Name.class); - - static { - primitiveTypeMap.put(Name.ASCII, new DataType.NativeType(Name.ASCII)); - primitiveTypeMap.put(Name.BIGINT, new DataType.NativeType(Name.BIGINT)); - primitiveTypeMap.put(Name.BLOB, new DataType.NativeType(Name.BLOB)); - primitiveTypeMap.put(Name.BOOLEAN, new DataType.NativeType(Name.BOOLEAN)); - primitiveTypeMap.put(Name.COUNTER, new DataType.NativeType(Name.COUNTER)); - primitiveTypeMap.put(Name.DECIMAL, new DataType.NativeType(Name.DECIMAL)); - primitiveTypeMap.put(Name.DOUBLE, new DataType.NativeType(Name.DOUBLE)); - primitiveTypeMap.put(Name.FLOAT, new DataType.NativeType(Name.FLOAT)); - primitiveTypeMap.put(Name.INET, new DataType.NativeType(Name.INET)); - primitiveTypeMap.put(Name.INT, new DataType.NativeType(Name.INT)); - primitiveTypeMap.put(Name.TEXT, new DataType.NativeType(Name.TEXT)); - primitiveTypeMap.put(Name.TIMESTAMP, new DataType.NativeType(Name.TIMESTAMP)); - primitiveTypeMap.put(Name.UUID, new DataType.NativeType(Name.UUID)); - primitiveTypeMap.put(Name.VARCHAR, new DataType.NativeType(Name.VARCHAR)); - primitiveTypeMap.put(Name.VARINT, new DataType.NativeType(Name.VARINT)); - primitiveTypeMap.put(Name.TIMEUUID, new DataType.NativeType(Name.TIMEUUID)); - primitiveTypeMap.put(Name.SMALLINT, new DataType.NativeType(Name.SMALLINT)); - primitiveTypeMap.put(Name.TINYINT, new DataType.NativeType(Name.TINYINT)); - primitiveTypeMap.put(Name.DATE, new DataType.NativeType(Name.DATE)); - primitiveTypeMap.put(Name.TIME, new DataType.NativeType(Name.TIME)); - primitiveTypeMap.put(Name.DURATION, new DataType.NativeType(Name.DURATION)); - } - - private static final Set primitiveTypeSet = - ImmutableSet.copyOf(primitiveTypeMap.values()); - - protected final DataType.Name name; - - protected DataType(DataType.Name name) { - this.name = name; - } - - static DataType decode( - ByteBuf buffer, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - Name name = Name.fromProtocolId(buffer.readUnsignedShort()); - switch (name) { - case CUSTOM: - String className = CBUtil.readString(buffer); - if (DataTypeClassNameParser.isDuration(className)) { - return DataType.duration(); - } else if (DataTypeClassNameParser.isUserType(className) - || DataTypeClassNameParser.isTupleType(className)) { - return DataTypeClassNameParser.parseOne(className, protocolVersion, codecRegistry); - } else { - return custom(className); - } - case LIST: - return list(decode(buffer, protocolVersion, codecRegistry)); - case SET: - return set(decode(buffer, protocolVersion, codecRegistry)); - case MAP: - DataType keys = decode(buffer, protocolVersion, codecRegistry); - DataType values = decode(buffer, protocolVersion, codecRegistry); - return map(keys, values); - case UDT: - String keyspace = CBUtil.readString(buffer); - String type = CBUtil.readString(buffer); - int nFields = buffer.readShort() & 0xffff; - List fields = new ArrayList(nFields); - for (int i = 0; i < nFields; i++) { - String fieldName = CBUtil.readString(buffer); - DataType fieldType = decode(buffer, protocolVersion, codecRegistry); - fields.add(new UserType.Field(fieldName, fieldType)); - } - return new UserType(keyspace, type, false, fields, protocolVersion, codecRegistry); - case TUPLE: - nFields = buffer.readShort() & 0xffff; - List types = new ArrayList(nFields); - for (int i = 0; i < nFields; i++) { - types.add(decode(buffer, protocolVersion, codecRegistry)); - } - return new TupleType(types, protocolVersion, codecRegistry); - default: - return primitiveTypeMap.get(name); - } - } - - /** - * Returns the ASCII type. - * - * @return The ASCII type. - */ - public static DataType ascii() { - return primitiveTypeMap.get(Name.ASCII); - } - - /** - * Returns the BIGINT type. - * - * @return The BIGINT type. - */ - public static DataType bigint() { - return primitiveTypeMap.get(Name.BIGINT); - } - - /** - * Returns the BLOB type. - * - * @return The BLOB type. - */ - public static DataType blob() { - return primitiveTypeMap.get(Name.BLOB); - } - - /** - * Returns the BOOLEAN type. - * - * @return The BOOLEAN type. - */ - public static DataType cboolean() { - return primitiveTypeMap.get(Name.BOOLEAN); - } - - /** - * Returns the COUNTER type. - * - * @return The COUNTER type. - */ - public static DataType counter() { - return primitiveTypeMap.get(Name.COUNTER); - } - - /** - * Returns the DECIMAL type. - * - * @return The DECIMAL type. - */ - public static DataType decimal() { - return primitiveTypeMap.get(Name.DECIMAL); - } - - /** - * Returns the DOUBLE type. - * - * @return The DOUBLE type. - */ - public static DataType cdouble() { - return primitiveTypeMap.get(Name.DOUBLE); - } - - /** - * Returns the FLOAT type. - * - * @return The FLOAT type. - */ - public static DataType cfloat() { - return primitiveTypeMap.get(Name.FLOAT); - } - - /** - * Returns the INET type. - * - * @return The INET type. - */ - public static DataType inet() { - return primitiveTypeMap.get(Name.INET); - } - - /** - * Returns the TINYINT type. - * - * @return The TINYINT type. - */ - public static DataType tinyint() { - return primitiveTypeMap.get(Name.TINYINT); - } - - /** - * Returns the SMALLINT type. - * - * @return The SMALLINT type. - */ - public static DataType smallint() { - return primitiveTypeMap.get(Name.SMALLINT); - } - - /** - * Returns the INT type. - * - * @return The INT type. - */ - public static DataType cint() { - return primitiveTypeMap.get(Name.INT); - } - - /** - * Returns the TEXT type. - * - * @return The TEXT type. - */ - public static DataType text() { - return primitiveTypeMap.get(Name.TEXT); - } - - /** - * Returns the TIMESTAMP type. - * - * @return The TIMESTAMP type. - */ - public static DataType timestamp() { - return primitiveTypeMap.get(Name.TIMESTAMP); - } - - /** - * Returns the DATE type. - * - * @return The DATE type. - */ - public static DataType date() { - return primitiveTypeMap.get(Name.DATE); - } - - /** - * Returns the TIME type. - * - * @return The TIME type. - */ - public static DataType time() { - return primitiveTypeMap.get(Name.TIME); - } - - /** - * Returns the UUID type. - * - * @return The UUID type. - */ - public static DataType uuid() { - return primitiveTypeMap.get(Name.UUID); - } - - /** - * Returns the VARCHAR type. - * - * @return The VARCHAR type. - */ - public static DataType varchar() { - return primitiveTypeMap.get(Name.VARCHAR); - } - - /** - * Returns the VARINT type. - * - * @return The VARINT type. - */ - public static DataType varint() { - return primitiveTypeMap.get(Name.VARINT); - } - - /** - * Returns the TIMEUUID type. - * - * @return The TIMEUUID type. - */ - public static DataType timeuuid() { - return primitiveTypeMap.get(Name.TIMEUUID); - } - - /** - * Returns the type of lists of {@code elementType} elements. - * - * @param elementType the type of the list elements. - * @param frozen whether the list is frozen. - * @return the type of lists of {@code elementType} elements. - */ - public static CollectionType list(DataType elementType, boolean frozen) { - return new DataType.CollectionType(Name.LIST, ImmutableList.of(elementType), frozen); - } - - /** - * Returns the type of "not frozen" lists of {@code elementType} elements. - * - *

This is a shorthand for {@code list(elementType, false);}. - * - * @param elementType the type of the list elements. - * @return the type of "not frozen" lists of {@code elementType} elements. - */ - public static CollectionType list(DataType elementType) { - return list(elementType, false); - } - - /** - * Returns the type of frozen lists of {@code elementType} elements. - * - *

This is a shorthand for {@code list(elementType, true);}. - * - * @param elementType the type of the list elements. - * @return the type of frozen lists of {@code elementType} elements. - */ - public static CollectionType frozenList(DataType elementType) { - return list(elementType, true); - } - - /** - * Returns the type of sets of {@code elementType} elements. - * - * @param elementType the type of the set elements. - * @param frozen whether the set is frozen. - * @return the type of sets of {@code elementType} elements. - */ - public static CollectionType set(DataType elementType, boolean frozen) { - return new DataType.CollectionType(Name.SET, ImmutableList.of(elementType), frozen); - } - - /** - * Returns the type of "not frozen" sets of {@code elementType} elements. - * - *

This is a shorthand for {@code set(elementType, false);}. - * - * @param elementType the type of the set elements. - * @return the type of "not frozen" sets of {@code elementType} elements. - */ - public static CollectionType set(DataType elementType) { - return set(elementType, false); - } - - /** - * Returns the type of frozen sets of {@code elementType} elements. - * - *

This is a shorthand for {@code set(elementType, true);}. - * - * @param elementType the type of the set elements. - * @return the type of frozen sets of {@code elementType} elements. - */ - public static CollectionType frozenSet(DataType elementType) { - return set(elementType, true); - } - - /** - * Returns the type of maps of {@code keyType} to {@code valueType} elements. - * - * @param keyType the type of the map keys. - * @param valueType the type of the map values. - * @param frozen whether the map is frozen. - * @return the type of maps of {@code keyType} to {@code valueType} elements. - */ - public static CollectionType map(DataType keyType, DataType valueType, boolean frozen) { - return new DataType.CollectionType(Name.MAP, ImmutableList.of(keyType, valueType), frozen); - } - - /** - * Returns the type of "not frozen" maps of {@code keyType} to {@code valueType} elements. - * - *

This is a shorthand for {@code map(keyType, valueType, false);}. - * - * @param keyType the type of the map keys. - * @param valueType the type of the map values. - * @return the type of "not frozen" maps of {@code keyType} to {@code valueType} elements. - */ - public static CollectionType map(DataType keyType, DataType valueType) { - return map(keyType, valueType, false); - } - - /** - * Returns the type of frozen maps of {@code keyType} to {@code valueType} elements. - * - *

This is a shorthand for {@code map(keyType, valueType, true);}. - * - * @param keyType the type of the map keys. - * @param valueType the type of the map values. - * @return the type of frozen maps of {@code keyType} to {@code valueType} elements. - */ - public static CollectionType frozenMap(DataType keyType, DataType valueType) { - return map(keyType, valueType, true); - } - - /** - * Returns a Custom type. - * - *

A custom type is defined by the name of the class used on the Cassandra side to implement - * it. Note that the support for custom types by the driver is limited. - * - *

The use of custom types is rarely useful and is thus not encouraged. - * - * @param typeClassName the server-side fully qualified class name for the type. - * @return the custom type for {@code typeClassName}. - */ - public static DataType.CustomType custom(String typeClassName) { - if (typeClassName == null) throw new NullPointerException(); - return new DataType.CustomType(Name.CUSTOM, typeClassName); - } - - /** - * Returns the Duration type, introduced in Cassandra 3.10. - * - *

Note that a Duration type does not have a native representation in CQL, and technically, is - * merely a special {@link DataType#custom(String) custom type} from the driver's point of view. - * - * @return the Duration type. The returned instance is a singleton. - */ - public static DataType duration() { - return primitiveTypeMap.get(Name.DURATION); - } - - /** - * Returns the name of that type. - * - * @return the name of that type. - */ - public Name getName() { - return name; - } - - /** - * Returns whether this data type is frozen. - * - *

This applies to User Defined Types, tuples and nested collections. Frozen types are - * serialized as a single value in Cassandra's storage engine, whereas non-frozen types are stored - * in a form that allows updates to individual subfields. - * - * @return whether this data type is frozen. - */ - public abstract boolean isFrozen(); - - /** - * Returns whether this data type represent a CQL {@link - * com.datastax.driver.core.DataType.CollectionType collection type}, that is, a list, set or map. - * - * @return whether this data type name represent the name of a collection type. - */ - public boolean isCollection() { - return this instanceof CollectionType; - } - - /** - * Returns the type arguments of this type. - * - *

Note that only the collection types (LIST, MAP, SET) have type arguments. For the other - * types, this will return an empty list. - * - *

For the collection types: - * - *

    - *
  • For lists and sets, this method returns one argument, the type of the elements. - *
  • For maps, this method returns two arguments, the first one is the type of the map keys, - * the second one is the type of the map values. - *
- * - * @return an immutable list containing the type arguments of this type. - */ - public List getTypeArguments() { - return Collections.emptyList(); - } - - /** - * Returns a set of all the primitive types, where primitive types are defined as the types that - * don't have type arguments (that is excluding lists, sets, maps, tuples and udts). - * - * @return returns a set of all the primitive types. - */ - public static Set allPrimitiveTypes() { - return primitiveTypeSet; - } - - /** - * Returns a String representation of this data type suitable for inclusion as a parameter type in - * a function or aggregate signature. - * - *

In such places, the String representation might vary from the canonical one as returned by - * {@link #toString()}; e.g. the {@code frozen} keyword is not accepted. - * - * @return a String representation of this data type suitable for inclusion as a parameter type in - * a function or aggregate signature. - */ - public String asFunctionParameterString() { - return toString(); - } - - /** Instances of this class represent CQL native types, also known as CQL primitive types. */ - public static class NativeType extends DataType { - - private NativeType(DataType.Name name) { - super(name); - } - - @Override - public boolean isFrozen() { - return false; - } - - @Override - public final int hashCode() { - return (name == Name.TEXT) ? Name.VARCHAR.hashCode() : name.hashCode(); - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof DataType.NativeType)) return false; - - NativeType that = (DataType.NativeType) o; - return this.name.isCompatibleWith(that.name); - } - - @Override - public String toString() { - return name.toString(); - } - } - - /** Instances of this class represent collection types, that is, lists, sets or maps. */ - public static class CollectionType extends DataType { - - private final List typeArguments; - private boolean frozen; - - private CollectionType(DataType.Name name, List typeArguments, boolean frozen) { - super(name); - this.typeArguments = typeArguments; - this.frozen = frozen; - } - - @Override - public boolean isFrozen() { - return frozen; - } - - @Override - public List getTypeArguments() { - return typeArguments; - } - - @Override - public final int hashCode() { - return MoreObjects.hashCode(name, typeArguments); - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof DataType.CollectionType)) return false; - - DataType.CollectionType d = (DataType.CollectionType) o; - return name == d.name && typeArguments.equals(d.typeArguments); - } - - @Override - public String toString() { - if (name == Name.MAP) { - String template = frozen ? "frozen<%s<%s, %s>>" : "%s<%s, %s>"; - return String.format(template, name, typeArguments.get(0), typeArguments.get(1)); - } else { - String template = frozen ? "frozen<%s<%s>>" : "%s<%s>"; - return String.format(template, name, typeArguments.get(0)); - } - } - - @Override - public String asFunctionParameterString() { - if (name == Name.MAP) { - String template = "%s<%s, %s>"; - return String.format( - template, - name, - typeArguments.get(0).asFunctionParameterString(), - typeArguments.get(1).asFunctionParameterString()); - } else { - String template = "%s<%s>"; - return String.format(template, name, typeArguments.get(0).asFunctionParameterString()); - } - } - } - - /** - * A "custom" type is a type that cannot be expressed as a CQL type. - * - *

Each custom type is merely identified by the fully qualified {@link - * #getCustomTypeClassName() class name} that represents this type server-side. - * - *

The driver provides a minimal support for such types through instances of this class. - * - *

A codec for custom types can be obtained via {@link TypeCodec#custom(DataType.CustomType)}. - */ - public static class CustomType extends DataType { - - private final String customClassName; - - private CustomType(DataType.Name name, String className) { - super(name); - this.customClassName = className; - } - - @Override - public boolean isFrozen() { - return false; - } - - /** - * Returns the fully qualified name of the subtype of {@code - * org.apache.cassandra.db.marshal.AbstractType} that represents this type server-side. - * - * @return the fully qualified name of the subtype of {@code - * org.apache.cassandra.db.marshal.AbstractType} that represents this type server-side. - */ - public String getCustomTypeClassName() { - return customClassName; - } - - @Override - public final int hashCode() { - return MoreObjects.hashCode(name, customClassName); - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof DataType.CustomType)) return false; - - DataType.CustomType d = (DataType.CustomType) o; - return name == d.name && MoreObjects.equal(customClassName, d.customClassName); - } - - @Override - public String toString() { - return String.format("'%s'", customClassName); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/DataTypeClassNameParser.java b/driver-core/src/main/java/com/datastax/driver/core/DataTypeClassNameParser.java deleted file mode 100644 index ef840356f3c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/DataTypeClassNameParser.java +++ /dev/null @@ -1,427 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.utils.Bytes; -import com.google.common.collect.ImmutableMap; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/* - * Parse data types from schema tables, for Cassandra 3.0 and above. - * In these versions, data types appear as class names, like "org.apache.cassandra.db.marshal.AsciiType" - * or "org.apache.cassandra.db.marshal.TupleType(org.apache.cassandra.db.marshal.Int32Type,org.apache.cassandra.db.marshal.Int32Type)". - * - * This is modified (and simplified) from Cassandra's TypeParser class to suit - * our needs. In particular it's not very efficient, but it doesn't really matter - * since it's rarely used and never in a critical path. - * - * Note that those methods all throw DriverInternalError when there is a parsing - * problem because in theory we'll only parse class names coming from Cassandra and - * so there shouldn't be anything wrong with them. - */ -class DataTypeClassNameParser { - private static final Logger logger = LoggerFactory.getLogger(DataTypeClassNameParser.class); - - private static final String REVERSED_TYPE = "org.apache.cassandra.db.marshal.ReversedType"; - private static final String FROZEN_TYPE = "org.apache.cassandra.db.marshal.FrozenType"; - private static final String COMPOSITE_TYPE = "org.apache.cassandra.db.marshal.CompositeType"; - private static final String COLLECTION_TYPE = - "org.apache.cassandra.db.marshal.ColumnToCollectionType"; - private static final String LIST_TYPE = "org.apache.cassandra.db.marshal.ListType"; - private static final String SET_TYPE = "org.apache.cassandra.db.marshal.SetType"; - private static final String MAP_TYPE = "org.apache.cassandra.db.marshal.MapType"; - private static final String UDT_TYPE = "org.apache.cassandra.db.marshal.UserType"; - private static final String TUPLE_TYPE = "org.apache.cassandra.db.marshal.TupleType"; - private static final String DURATION_TYPE = "org.apache.cassandra.db.marshal.DurationType"; - - private static ImmutableMap cassTypeToDataType = - new ImmutableMap.Builder() - .put("org.apache.cassandra.db.marshal.AsciiType", DataType.ascii()) - .put("org.apache.cassandra.db.marshal.LongType", DataType.bigint()) - .put("org.apache.cassandra.db.marshal.BytesType", DataType.blob()) - .put("org.apache.cassandra.db.marshal.BooleanType", DataType.cboolean()) - .put("org.apache.cassandra.db.marshal.CounterColumnType", DataType.counter()) - .put("org.apache.cassandra.db.marshal.DecimalType", DataType.decimal()) - .put("org.apache.cassandra.db.marshal.DoubleType", DataType.cdouble()) - .put("org.apache.cassandra.db.marshal.FloatType", DataType.cfloat()) - .put("org.apache.cassandra.db.marshal.InetAddressType", DataType.inet()) - .put("org.apache.cassandra.db.marshal.Int32Type", DataType.cint()) - .put("org.apache.cassandra.db.marshal.UTF8Type", DataType.text()) - .put("org.apache.cassandra.db.marshal.TimestampType", DataType.timestamp()) - .put("org.apache.cassandra.db.marshal.SimpleDateType", DataType.date()) - .put("org.apache.cassandra.db.marshal.TimeType", DataType.time()) - .put("org.apache.cassandra.db.marshal.UUIDType", DataType.uuid()) - .put("org.apache.cassandra.db.marshal.IntegerType", DataType.varint()) - .put("org.apache.cassandra.db.marshal.TimeUUIDType", DataType.timeuuid()) - .put("org.apache.cassandra.db.marshal.ByteType", DataType.tinyint()) - .put("org.apache.cassandra.db.marshal.ShortType", DataType.smallint()) - .put(DURATION_TYPE, DataType.duration()) - .build(); - - static DataType parseOne( - String className, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - boolean frozen = false; - if (isReversed(className)) { - // Just skip the ReversedType part, we don't care - className = getNestedClassName(className); - } else if (isFrozen(className)) { - frozen = true; - className = getNestedClassName(className); - } - - Parser parser = new Parser(className, 0); - String next = parser.parseNextName(); - - if (next.startsWith(LIST_TYPE)) - return DataType.list( - parseOne(parser.getTypeParameters().get(0), protocolVersion, codecRegistry), frozen); - - if (next.startsWith(SET_TYPE)) - return DataType.set( - parseOne(parser.getTypeParameters().get(0), protocolVersion, codecRegistry), frozen); - - if (next.startsWith(MAP_TYPE)) { - List params = parser.getTypeParameters(); - return DataType.map( - parseOne(params.get(0), protocolVersion, codecRegistry), - parseOne(params.get(1), protocolVersion, codecRegistry), - frozen); - } - - if (frozen) - logger.warn( - "Got o.a.c.db.marshal.FrozenType for something else than a collection, " - + "this driver version might be too old for your version of Cassandra"); - - if (isUserType(next)) { - ++parser.idx; // skipping '(' - - String keyspace = parser.readOne(); - parser.skipBlankAndComma(); - String typeName = - TypeCodec.varchar() - .deserialize(Bytes.fromHexString("0x" + parser.readOne()), protocolVersion); - parser.skipBlankAndComma(); - Map rawFields = parser.getNameAndTypeParameters(); - List fields = new ArrayList(rawFields.size()); - for (Map.Entry entry : rawFields.entrySet()) - fields.add( - new UserType.Field( - entry.getKey(), parseOne(entry.getValue(), protocolVersion, codecRegistry))); - // create a frozen UserType since C* 2.x UDTs are always frozen. - return new UserType(keyspace, typeName, true, fields, protocolVersion, codecRegistry); - } - - if (isTupleType(next)) { - List rawTypes = parser.getTypeParameters(); - List types = new ArrayList(rawTypes.size()); - for (String rawType : rawTypes) { - types.add(parseOne(rawType, protocolVersion, codecRegistry)); - } - return new TupleType(types, protocolVersion, codecRegistry); - } - - DataType type = cassTypeToDataType.get(next); - return type == null ? DataType.custom(className) : type; - } - - public static boolean isReversed(String className) { - return className.startsWith(REVERSED_TYPE); - } - - public static boolean isFrozen(String className) { - return className.startsWith(FROZEN_TYPE); - } - - private static String getNestedClassName(String className) { - Parser p = new Parser(className, 0); - p.parseNextName(); - List l = p.getTypeParameters(); - if (l.size() != 1) throw new IllegalStateException(); - className = l.get(0); - return className; - } - - public static boolean isUserType(String className) { - return className.startsWith(UDT_TYPE); - } - - public static boolean isTupleType(String className) { - return className.startsWith(TUPLE_TYPE); - } - - private static boolean isComposite(String className) { - return className.startsWith(COMPOSITE_TYPE); - } - - private static boolean isCollection(String className) { - return className.startsWith(COLLECTION_TYPE); - } - - public static boolean isDuration(String className) { - return className.equals(DURATION_TYPE); - } - - static ParseResult parseWithComposite( - String className, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - Parser parser = new Parser(className, 0); - - String next = parser.parseNextName(); - if (!isComposite(next)) - return new ParseResult(parseOne(className, protocolVersion, codecRegistry), isReversed(next)); - - List subClassNames = parser.getTypeParameters(); - int count = subClassNames.size(); - String last = subClassNames.get(count - 1); - Map collections = new HashMap(); - if (isCollection(last)) { - count--; - Parser collectionParser = new Parser(last, 0); - collectionParser.parseNextName(); // skips columnToCollectionType - Map params = collectionParser.getCollectionsParameters(); - for (Map.Entry entry : params.entrySet()) - collections.put(entry.getKey(), parseOne(entry.getValue(), protocolVersion, codecRegistry)); - } - - List types = new ArrayList(count); - List reversed = new ArrayList(count); - for (int i = 0; i < count; i++) { - types.add(parseOne(subClassNames.get(i), protocolVersion, codecRegistry)); - reversed.add(isReversed(subClassNames.get(i))); - } - - return new ParseResult(true, types, reversed, collections); - } - - static class ParseResult { - public final boolean isComposite; - public final List types; - public final List reversed; - public final Map collections; - - private ParseResult(DataType type, boolean reversed) { - this( - false, - Collections.singletonList(type), - Collections.singletonList(reversed), - Collections.emptyMap()); - } - - private ParseResult( - boolean isComposite, - List types, - List reversed, - Map collections) { - this.isComposite = isComposite; - this.types = types; - this.reversed = reversed; - this.collections = collections; - } - } - - private static class Parser { - - private final String str; - private int idx; - - private Parser(String str, int idx) { - this.str = str; - this.idx = idx; - } - - public String parseNextName() { - skipBlank(); - return readNextIdentifier(); - } - - public String readOne() { - String name = parseNextName(); - String args = readRawArguments(); - return name + args; - } - - // Assumes we have just read a class name and read it's potential arguments - // blindly. I.e. it assume that either parsing is done or that we're on a '(' - // and this reads everything up until the corresponding closing ')'. It - // returns everything read, including the enclosing parenthesis. - private String readRawArguments() { - skipBlank(); - - if (isEOS() || str.charAt(idx) == ')' || str.charAt(idx) == ',') return ""; - - if (str.charAt(idx) != '(') - throw new IllegalStateException( - String.format( - "Expecting char %d of %s to be '(' but '%c' found", idx, str, str.charAt(idx))); - - int i = idx; - int open = 1; - while (open > 0) { - ++idx; - - if (isEOS()) throw new IllegalStateException("Non closed parenthesis"); - - if (str.charAt(idx) == '(') { - open++; - } else if (str.charAt(idx) == ')') { - open--; - } - } - // we've stopped at the last closing ')' so move past that - ++idx; - return str.substring(i, idx); - } - - public List getTypeParameters() { - List list = new ArrayList(); - - if (isEOS()) return list; - - if (str.charAt(idx) != '(') throw new IllegalStateException(); - - ++idx; // skipping '(' - - while (skipBlankAndComma()) { - if (str.charAt(idx) == ')') { - ++idx; - return list; - } - - try { - list.add(readOne()); - } catch (DriverInternalError e) { - throw new DriverInternalError( - String.format("Exception while parsing '%s' around char %d", str, idx), e); - } - } - throw new DriverInternalError( - String.format( - "Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); - } - - public Map getCollectionsParameters() { - if (isEOS()) return Collections.emptyMap(); - - if (str.charAt(idx) != '(') throw new IllegalStateException(); - - ++idx; // skipping '(' - - return getNameAndTypeParameters(); - } - - // Must be at the start of the first parameter to read - public Map getNameAndTypeParameters() { - // The order of the hashmap matters for UDT - Map map = new LinkedHashMap(); - - while (skipBlankAndComma()) { - if (str.charAt(idx) == ')') { - ++idx; - return map; - } - - String bbHex = readNextIdentifier(); - String name = null; - try { - name = - TypeCodec.varchar() - .deserialize(Bytes.fromHexString("0x" + bbHex), ProtocolVersion.NEWEST_SUPPORTED); - } catch (NumberFormatException e) { - throwSyntaxError(e.getMessage()); - } - - skipBlank(); - if (str.charAt(idx) != ':') throwSyntaxError("expecting ':' token"); - - ++idx; - skipBlank(); - try { - map.put(name, readOne()); - } catch (DriverInternalError e) { - throw new DriverInternalError( - String.format("Exception while parsing '%s' around char %d", str, idx), e); - } - } - throw new DriverInternalError( - String.format( - "Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); - } - - private void throwSyntaxError(String msg) { - throw new DriverInternalError( - String.format("Syntax error parsing '%s' at char %d: %s", str, idx, msg)); - } - - private boolean isEOS() { - return isEOS(str, idx); - } - - private static boolean isEOS(String str, int i) { - return i >= str.length(); - } - - private void skipBlank() { - idx = skipBlank(str, idx); - } - - private static int skipBlank(String str, int i) { - while (!isEOS(str, i) && ParseUtils.isBlank(str.charAt(i))) ++i; - - return i; - } - - // skip all blank and at best one comma, return true if there not EOS - private boolean skipBlankAndComma() { - boolean commaFound = false; - while (!isEOS()) { - int c = str.charAt(idx); - if (c == ',') { - if (commaFound) return true; - else commaFound = true; - } else if (!ParseUtils.isBlank(c)) { - return true; - } - ++idx; - } - return false; - } - - // left idx positioned on the character stopping the read - public String readNextIdentifier() { - int i = idx; - while (!isEOS() && ParseUtils.isIdentifierChar(str.charAt(idx))) ++idx; - - return str.substring(i, idx); - } - - @Override - public String toString() { - return str.substring(0, idx) - + "[" - + (idx == str.length() ? "" : str.charAt(idx)) - + "]" - + str.substring(idx + 1); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/DataTypeCqlNameParser.java b/driver-core/src/main/java/com/datastax/driver/core/DataTypeCqlNameParser.java deleted file mode 100644 index 1137d2358bc..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/DataTypeCqlNameParser.java +++ /dev/null @@ -1,386 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.DataType.ascii; -import static com.datastax.driver.core.DataType.bigint; -import static com.datastax.driver.core.DataType.blob; -import static com.datastax.driver.core.DataType.cboolean; -import static com.datastax.driver.core.DataType.cdouble; -import static com.datastax.driver.core.DataType.cfloat; -import static com.datastax.driver.core.DataType.cint; -import static com.datastax.driver.core.DataType.counter; -import static com.datastax.driver.core.DataType.custom; -import static com.datastax.driver.core.DataType.date; -import static com.datastax.driver.core.DataType.decimal; -import static com.datastax.driver.core.DataType.duration; -import static com.datastax.driver.core.DataType.inet; -import static com.datastax.driver.core.DataType.list; -import static com.datastax.driver.core.DataType.map; -import static com.datastax.driver.core.DataType.set; -import static com.datastax.driver.core.DataType.smallint; -import static com.datastax.driver.core.DataType.text; -import static com.datastax.driver.core.DataType.time; -import static com.datastax.driver.core.DataType.timestamp; -import static com.datastax.driver.core.DataType.timeuuid; -import static com.datastax.driver.core.DataType.tinyint; -import static com.datastax.driver.core.DataType.uuid; -import static com.datastax.driver.core.DataType.varchar; -import static com.datastax.driver.core.DataType.varint; -import static com.datastax.driver.core.ParseUtils.isBlank; -import static com.datastax.driver.core.ParseUtils.isIdentifierChar; -import static com.datastax.driver.core.ParseUtils.skipSpaces; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.exceptions.UnresolvedUserTypeException; -import com.google.common.collect.ImmutableMap; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/* - * Parse data types from schema tables, for Cassandra 3.0 and above. - * In these versions, data types appear as string literals, like "ascii" or "tuple". - * - * Note that these methods all throw DriverInternalError when there is a parsing - * problem because in theory we'll only parse class names coming from Cassandra and - * so there shouldn't be anything wrong with them. - */ -class DataTypeCqlNameParser { - - private static final String FROZEN = "frozen"; - private static final String LIST = "list"; - private static final String SET = "set"; - private static final String MAP = "map"; - private static final String TUPLE = "tuple"; - private static final String EMPTY = "empty"; - - private static final ImmutableMap NATIVE_TYPES_MAP = - new ImmutableMap.Builder() - .put("ascii", ascii()) - .put("bigint", bigint()) - .put("blob", blob()) - .put("boolean", cboolean()) - .put("counter", counter()) - .put("decimal", decimal()) - .put("double", cdouble()) - .put("float", cfloat()) - .put("inet", inet()) - .put("int", cint()) - .put("text", text()) - .put("varchar", varchar()) - .put("timestamp", timestamp()) - .put("date", date()) - .put("time", time()) - .put("uuid", uuid()) - .put("varint", varint()) - .put("timeuuid", timeuuid()) - .put("tinyint", tinyint()) - .put("smallint", smallint()) - // duration is not really a native CQL type, but appears as so in system tables - .put("duration", duration()) - .build(); - - /** - * @param currentUserTypes if this method gets called as part of a refresh that spans multiple - * user types, this contains the ones that have already been refreshed. If the type we are - * parsing references a user type, we want to pick its definition from this map in priority. - * @param oldUserTypes this contains all the keyspace's user types as they were before the refresh - * started. If we can't find a definition in {@code currentUserTypes}, we'll check this map as - * a fallback. - */ - static DataType parse( - String toParse, - Cluster cluster, - String currentKeyspaceName, - Map currentUserTypes, - Map oldUserTypes, - boolean frozen, - boolean shallowUserTypes) { - - if (toParse.startsWith("'")) return custom(toParse.substring(1, toParse.length() - 1)); - - Parser parser = new Parser(toParse, 0); - String type = parser.parseTypeName(); - - DataType nativeType = NATIVE_TYPES_MAP.get(type.toLowerCase()); - if (nativeType != null) return nativeType; - - if (parser.isEOS()) { - // return a custom type for the special empty type - // so that it gets detected later on, see TableMetadata - if (type.equalsIgnoreCase(EMPTY)) return custom(type); - - // We need to remove escaped double quotes within the type name as it is stored unescaped. - // Otherwise it's a UDT. If we only want a shallow definition build it, otherwise search known - // definitions. - if (shallowUserTypes) - return new UserType.Shallow(currentKeyspaceName, Metadata.handleId(type), frozen); - - UserType userType = null; - if (currentUserTypes != null) userType = currentUserTypes.get(Metadata.handleId(type)); - if (userType == null && oldUserTypes != null) - userType = oldUserTypes.get(Metadata.handleId(type)); - - if (userType == null) throw new UnresolvedUserTypeException(currentKeyspaceName, type); - else return userType.copy(frozen); - } - - List parameters = parser.parseTypeParameters(); - if (type.equalsIgnoreCase(LIST)) { - if (parameters.size() != 1) - throw new DriverInternalError( - String.format("Excepting single parameter for list, got %s", parameters)); - DataType elementType = - parse( - parameters.get(0), - cluster, - currentKeyspaceName, - currentUserTypes, - oldUserTypes, - false, - shallowUserTypes); - return list(elementType, frozen); - } - - if (type.equalsIgnoreCase(SET)) { - if (parameters.size() != 1) - throw new DriverInternalError( - String.format("Excepting single parameter for set, got %s", parameters)); - DataType elementType = - parse( - parameters.get(0), - cluster, - currentKeyspaceName, - currentUserTypes, - oldUserTypes, - false, - shallowUserTypes); - return set(elementType, frozen); - } - - if (type.equalsIgnoreCase(MAP)) { - if (parameters.size() != 2) - throw new DriverInternalError( - String.format("Excepting two parameters for map, got %s", parameters)); - DataType keyType = - parse( - parameters.get(0), - cluster, - currentKeyspaceName, - currentUserTypes, - oldUserTypes, - false, - shallowUserTypes); - DataType valueType = - parse( - parameters.get(1), - cluster, - currentKeyspaceName, - currentUserTypes, - oldUserTypes, - false, - shallowUserTypes); - return map(keyType, valueType, frozen); - } - - if (type.equalsIgnoreCase(FROZEN)) { - if (parameters.size() != 1) - throw new DriverInternalError( - String.format("Excepting single parameter for frozen keyword, got %s", parameters)); - return parse( - parameters.get(0), - cluster, - currentKeyspaceName, - currentUserTypes, - oldUserTypes, - true, - shallowUserTypes); - } - - if (type.equalsIgnoreCase(TUPLE)) { - if (parameters.isEmpty()) { - throw new IllegalArgumentException("Expecting at list one parameter for tuple, got none"); - } - List types = new ArrayList(parameters.size()); - for (String rawType : parameters) { - types.add( - parse( - rawType, - cluster, - currentKeyspaceName, - currentUserTypes, - oldUserTypes, - false, - shallowUserTypes)); - } - return cluster.getMetadata().newTupleType(types); - } - - throw new IllegalArgumentException("Could not parse type name " + toParse); - } - - private static class Parser { - - private final String str; - - private int idx; - - Parser(String str, int idx) { - this.str = str; - this.idx = idx; - } - - String parseTypeName() { - idx = skipSpaces(str, idx); - return readNextIdentifier(); - } - - List parseTypeParameters() { - List list = new ArrayList(); - - if (isEOS()) return list; - - skipBlankAndComma(); - - if (str.charAt(idx) != '<') throw new IllegalStateException(); - - ++idx; // skipping '<' - - while (skipBlankAndComma()) { - if (str.charAt(idx) == '>') { - ++idx; - return list; - } - - try { - String name = parseTypeName(); - String args = readRawTypeParameters(); - list.add(name + args); - } catch (DriverInternalError e) { - DriverInternalError ex = - new DriverInternalError( - String.format("Exception while parsing '%s' around char %d", str, idx)); - ex.initCause(e); - throw ex; - } - } - throw new DriverInternalError( - String.format( - "Syntax error parsing '%s' at char %d: unexpected end of string", str, idx)); - } - - // left idx positioned on the character stopping the read - private String readNextIdentifier() { - int startIdx = idx; - if (str.charAt(startIdx) == '"') { // case-sensitive name included in double quotes - ++idx; - // read until closing quote. - while (!isEOS()) { - boolean atQuote = str.charAt(idx) == '"'; - ++idx; - if (atQuote) { - // if the next character is also a quote, this is an escaped - // quote, continue reading, otherwise stop. - if (!isEOS() && str.charAt(idx) == '"') ++idx; - else break; - } - } - } else if (str.charAt(startIdx) == '\'') { // custom type name included in single quotes - ++idx; - // read until closing quote. - while (!isEOS() && str.charAt(idx++) != '\'') { - /* loop */ - } - } else { - while (!isEOS() && (isIdentifierChar(str.charAt(idx)) || str.charAt(idx) == '"')) ++idx; - } - return str.substring(startIdx, idx); - } - - // Assumes we have just read a type name and read it's potential arguments - // blindly. I.e. it assume that either parsing is done or that we're on a '<' - // and this reads everything up until the corresponding closing '>'. It - // returns everything read, including the enclosing brackets. - private String readRawTypeParameters() { - idx = skipSpaces(str, idx); - - if (isEOS() || str.charAt(idx) == '>' || str.charAt(idx) == ',') return ""; - - if (str.charAt(idx) != '<') - throw new IllegalStateException( - String.format( - "Expecting char %d of %s to be '<' but '%c' found", idx, str, str.charAt(idx))); - - int i = idx; - int open = 1; - boolean inQuotes = false; - while (open > 0) { - ++idx; - - if (isEOS()) throw new IllegalStateException("Non closed angle brackets"); - - // Only parse for '<' and '>' characters if not within a quoted identifier. - // Note we don't need to handle escaped quotes ("") in type names here, because they just - // cause inQuotes to flip - // to false and immediately back to true - if (!inQuotes) { - if (str.charAt(idx) == '"') { - inQuotes = true; - } else if (str.charAt(idx) == '<') { - open++; - } else if (str.charAt(idx) == '>') { - open--; - } - } else if (str.charAt(idx) == '"') { - inQuotes = false; - } - } - // we've stopped at the last closing ')' so move past that - ++idx; - return str.substring(i, idx); - } - - // skip all blank and at best one comma, return true if there not EOS - private boolean skipBlankAndComma() { - boolean commaFound = false; - while (!isEOS()) { - int c = str.charAt(idx); - if (c == ',') { - if (commaFound) return true; - else commaFound = true; - } else if (!isBlank(c)) { - return true; - } - ++idx; - } - return false; - } - - private boolean isEOS() { - return idx >= str.length(); - } - - @Override - public String toString() { - return str.substring(0, idx) - + "[" - + (idx == str.length() ? "" : str.charAt(idx)) - + "]" - + str.substring(idx + 1); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/DefaultEndPointFactory.java b/driver-core/src/main/java/com/datastax/driver/core/DefaultEndPointFactory.java deleted file mode 100644 index b9caa3ea1a4..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/DefaultEndPointFactory.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class DefaultEndPointFactory implements EndPointFactory { - - private static final Logger logger = LoggerFactory.getLogger(ControlConnection.class); - private static final InetAddress BIND_ALL_ADDRESS; - - static { - try { - BIND_ALL_ADDRESS = InetAddress.getByAddress(new byte[4]); - } catch (UnknownHostException e) { - throw new RuntimeException(e); - } - } - - private volatile Cluster cluster; - - @Override - public void init(Cluster cluster) { - this.cluster = cluster; - } - - @Override - public EndPoint create(Row peersRow) { - if (peersRow.getColumnDefinitions().contains("native_address")) { - InetAddress nativeAddress = peersRow.getInet("native_address"); - int nativePort = peersRow.getInt("native_port"); - InetSocketAddress translateAddress = - cluster.manager.translateAddress(new InetSocketAddress(nativeAddress, nativePort)); - return new TranslatedAddressEndPoint(translateAddress); - } else if (peersRow.getColumnDefinitions().contains("native_transport_address")) { - InetAddress nativeAddress = peersRow.getInet("native_transport_address"); - int nativePort = peersRow.getInt("native_transport_port"); - if (cluster.getConfiguration().getProtocolOptions().getSSLOptions() != null - && !peersRow.isNull("native_transport_port_ssl")) { - nativePort = peersRow.getInt("native_transport_port_ssl"); - } - InetSocketAddress translateAddress = - cluster.manager.translateAddress(new InetSocketAddress(nativeAddress, nativePort)); - return new TranslatedAddressEndPoint(translateAddress); - } else { - InetAddress broadcastAddress = peersRow.getInet("peer"); - InetAddress rpcAddress = peersRow.getInet("rpc_address"); - if (broadcastAddress == null || rpcAddress == null) { - return null; - } else if (rpcAddress.equals(BIND_ALL_ADDRESS)) { - logger.warn( - "Found host with 0.0.0.0 as rpc_address, " - + "using broadcast_address ({}) to contact it instead. " - + "If this is incorrect you should avoid the use of 0.0.0.0 server side.", - broadcastAddress); - rpcAddress = broadcastAddress; - } - InetSocketAddress translateAddress = cluster.manager.translateAddress(rpcAddress); - return new TranslatedAddressEndPoint(translateAddress); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/DefaultPreparedStatement.java b/driver-core/src/main/java/com/datastax/driver/core/DefaultPreparedStatement.java deleted file mode 100644 index 6ce183fbea3..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/DefaultPreparedStatement.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.ProtocolVersion.V4; - -import com.datastax.driver.core.policies.RetryPolicy; -import com.google.common.collect.ImmutableMap; -import java.nio.ByteBuffer; -import java.util.List; -import java.util.Map; - -public class DefaultPreparedStatement implements PreparedStatement { - - final PreparedId preparedId; - - final String query; - final String queryKeyspace; - final Map incomingPayload; - final Cluster cluster; - - volatile ByteBuffer routingKey; - - volatile ConsistencyLevel consistency; - volatile ConsistencyLevel serialConsistency; - volatile boolean traceQuery; - volatile RetryPolicy retryPolicy; - volatile ImmutableMap outgoingPayload; - volatile Boolean idempotent; - - private DefaultPreparedStatement( - PreparedId id, - String query, - String queryKeyspace, - Map incomingPayload, - Cluster cluster) { - this.preparedId = id; - this.query = query; - this.queryKeyspace = queryKeyspace; - this.incomingPayload = incomingPayload; - this.cluster = cluster; - } - - static DefaultPreparedStatement fromMessage( - Responses.Result.Prepared msg, Cluster cluster, String query, String queryKeyspace) { - assert msg.metadata.columns != null; - - ColumnDefinitions defs = msg.metadata.columns; - - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - PreparedId.PreparedMetadata boundValuesMetadata = - new PreparedId.PreparedMetadata(msg.statementId, defs); - PreparedId.PreparedMetadata resultSetMetadata = - new PreparedId.PreparedMetadata(msg.resultMetadataId, msg.resultMetadata.columns); - - int[] pkIndices = null; - if (defs.size() > 0) { - pkIndices = - (protocolVersion.compareTo(V4) >= 0) - ? msg.metadata.pkIndices - : computePkIndices(cluster.getMetadata(), defs); - } - - PreparedId preparedId = - new PreparedId(boundValuesMetadata, resultSetMetadata, pkIndices, protocolVersion); - return new DefaultPreparedStatement( - preparedId, query, queryKeyspace, msg.getCustomPayload(), cluster); - } - - private static int[] computePkIndices(Metadata clusterMetadata, ColumnDefinitions boundColumns) { - List partitionKeyColumns = null; - int[] pkIndexes = null; - KeyspaceMetadata km = clusterMetadata.getKeyspace(Metadata.quote(boundColumns.getKeyspace(0))); - if (km != null) { - TableMetadata tm = km.getTable(Metadata.quote(boundColumns.getTable(0))); - if (tm != null) { - partitionKeyColumns = tm.getPartitionKey(); - pkIndexes = new int[partitionKeyColumns.size()]; - for (int i = 0; i < pkIndexes.length; ++i) pkIndexes[i] = -1; - } - } - - // Note: we rely on the fact CQL queries cannot span multiple tables. If that change, we'll have - // to get smarter. - for (int i = 0; i < boundColumns.size(); i++) - maybeGetIndex(boundColumns.getName(i), i, partitionKeyColumns, pkIndexes); - - return allSet(pkIndexes) ? pkIndexes : null; - } - - private static void maybeGetIndex( - String name, int j, List pkColumns, int[] pkIndexes) { - if (pkColumns == null) return; - - for (int i = 0; i < pkColumns.size(); ++i) { - if (name.equals(pkColumns.get(i).getName())) { - // We may have the same column prepared multiple times, but only pick the first value - pkIndexes[i] = j; - return; - } - } - } - - private static boolean allSet(int[] pkColumns) { - if (pkColumns == null) return false; - - for (int i = 0; i < pkColumns.length; ++i) if (pkColumns[i] < 0) return false; - - return true; - } - - @Override - public ColumnDefinitions getVariables() { - return preparedId.boundValuesMetadata.variables; - } - - @Override - public BoundStatement bind(Object... values) { - BoundStatement bs = new BoundStatement(this); - return bs.bind(values); - } - - @Override - public BoundStatement bind() { - return new BoundStatement(this); - } - - @Override - public PreparedStatement setRoutingKey(ByteBuffer routingKey) { - this.routingKey = routingKey; - return this; - } - - @Override - public PreparedStatement setRoutingKey(ByteBuffer... routingKeyComponents) { - this.routingKey = SimpleStatement.compose(routingKeyComponents); - return this; - } - - @Override - public ByteBuffer getRoutingKey() { - return routingKey; - } - - @Override - public PreparedStatement setConsistencyLevel(ConsistencyLevel consistency) { - this.consistency = consistency; - return this; - } - - @Override - public ConsistencyLevel getConsistencyLevel() { - return consistency; - } - - @Override - public PreparedStatement setSerialConsistencyLevel(ConsistencyLevel serialConsistency) { - if (!serialConsistency.isSerial()) throw new IllegalArgumentException(); - this.serialConsistency = serialConsistency; - return this; - } - - @Override - public ConsistencyLevel getSerialConsistencyLevel() { - return serialConsistency; - } - - @Override - public String getQueryString() { - return query; - } - - @Override - public String getQueryKeyspace() { - return queryKeyspace; - } - - @Override - public PreparedStatement enableTracing() { - this.traceQuery = true; - return this; - } - - @Override - public PreparedStatement disableTracing() { - this.traceQuery = false; - return this; - } - - @Override - public boolean isTracing() { - return traceQuery; - } - - @Override - public PreparedStatement setRetryPolicy(RetryPolicy policy) { - this.retryPolicy = policy; - return this; - } - - @Override - public RetryPolicy getRetryPolicy() { - return retryPolicy; - } - - @Override - public PreparedId getPreparedId() { - return preparedId; - } - - @Override - public Map getIncomingPayload() { - return incomingPayload; - } - - @Override - public Map getOutgoingPayload() { - return outgoingPayload; - } - - @Override - public PreparedStatement setOutgoingPayload(Map payload) { - this.outgoingPayload = payload == null ? null : ImmutableMap.copyOf(payload); - return this; - } - - @Override - public CodecRegistry getCodecRegistry() { - return cluster.getConfiguration().getCodecRegistry(); - } - - /** {@inheritDoc} */ - @Override - public PreparedStatement setIdempotent(Boolean idempotent) { - this.idempotent = idempotent; - return this; - } - - /** {@inheritDoc} */ - @Override - public Boolean isIdempotent() { - return this.idempotent; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/DefaultResultSetFuture.java b/driver-core/src/main/java/com/datastax/driver/core/DefaultResultSetFuture.java deleted file mode 100644 index 13ac90cecac..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/DefaultResultSetFuture.java +++ /dev/null @@ -1,377 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.SchemaElement.KEYSPACE; - -import com.datastax.driver.core.exceptions.ConnectionException; -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.exceptions.NoHostAvailableException; -import com.datastax.driver.core.exceptions.OperationTimedOutException; -import com.datastax.driver.core.exceptions.QueryExecutionException; -import com.datastax.driver.core.exceptions.QueryValidationException; -import com.google.common.util.concurrent.AbstractFuture; -import com.google.common.util.concurrent.Uninterruptibles; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Internal implementation of ResultSetFuture. */ -class DefaultResultSetFuture extends AbstractFuture - implements ResultSetFuture, RequestHandler.Callback { - - private static final Logger logger = LoggerFactory.getLogger(ResultSetFuture.class); - - private final SessionManager session; - private final ProtocolVersion protocolVersion; - private final Message.Request request; - private volatile RequestHandler handler; - - DefaultResultSetFuture( - SessionManager session, ProtocolVersion protocolVersion, Message.Request request) { - this.session = session; - this.protocolVersion = protocolVersion; - this.request = request; - } - - @Override - public void register(RequestHandler handler) { - this.handler = handler; - } - - @Override - public Message.Request request() { - return request; - } - - @Override - public void onSet( - Connection connection, - Message.Response response, - ExecutionInfo info, - Statement statement, - long latency) { - try { - switch (response.type) { - case RESULT: - Responses.Result rm = (Responses.Result) response; - switch (rm.kind) { - case SET_KEYSPACE: - // propagate the keyspace change to other connections - session.poolsState.setKeyspace(((Responses.Result.SetKeyspace) rm).keyspace); - set(ArrayBackedResultSet.fromMessage(rm, session, protocolVersion, info, statement)); - break; - case SCHEMA_CHANGE: - ResultSet rs = - ArrayBackedResultSet.fromMessage(rm, session, protocolVersion, info, statement); - final Cluster.Manager cluster = session.cluster.manager; - if (!cluster.configuration.getQueryOptions().isMetadataEnabled()) { - cluster.waitForSchemaAgreementAndSignal(connection, this, rs); - } else { - Responses.Result.SchemaChange scc = (Responses.Result.SchemaChange) rm; - switch (scc.change) { - case CREATED: - case UPDATED: - cluster.refreshSchemaAndSignal( - connection, - this, - rs, - scc.targetType, - scc.targetKeyspace, - scc.targetName, - scc.targetSignature); - break; - case DROPPED: - if (scc.targetType == KEYSPACE) { - // If that the one keyspace we are logged in, reset to null (it shouldn't - // really happen but ...) - // Note: Actually, Cassandra doesn't do that so we don't either as this could - // confuse prepared statements. - // We'll add it back if CASSANDRA-5358 changes that behavior - // if (scc.keyspace.equals(session.poolsState.keyspace)) - // session.poolsState.setKeyspace(null); - final KeyspaceMetadata removedKeyspace = - cluster.metadata.removeKeyspace(scc.targetKeyspace); - if (removedKeyspace != null) { - cluster.executor.submit( - new Runnable() { - @Override - public void run() { - cluster.metadata.triggerOnKeyspaceRemoved(removedKeyspace); - } - }); - } - } else { - KeyspaceMetadata keyspace = - session.cluster.manager.metadata.keyspaces.get(scc.targetKeyspace); - if (keyspace == null) { - logger.warn( - "Received a DROPPED notification for {} {}.{}, but this keyspace is unknown in our metadata", - scc.targetType, - scc.targetKeyspace, - scc.targetName); - } else { - switch (scc.targetType) { - case TABLE: - // we can't tell whether it's a table or a view, - // but since two objects cannot have the same name, - // try removing both - final TableMetadata removedTable = keyspace.removeTable(scc.targetName); - if (removedTable != null) { - cluster.executor.submit( - new Runnable() { - @Override - public void run() { - cluster.metadata.triggerOnTableRemoved(removedTable); - } - }); - } else { - final MaterializedViewMetadata removedView = - keyspace.removeMaterializedView(scc.targetName); - if (removedView != null) { - cluster.executor.submit( - new Runnable() { - @Override - public void run() { - cluster.metadata.triggerOnMaterializedViewRemoved( - removedView); - } - }); - } - } - break; - case TYPE: - final UserType removedType = keyspace.removeUserType(scc.targetName); - if (removedType != null) { - cluster.executor.submit( - new Runnable() { - @Override - public void run() { - cluster.metadata.triggerOnUserTypeRemoved(removedType); - } - }); - } - break; - case FUNCTION: - final FunctionMetadata removedFunction = - keyspace.removeFunction( - Metadata.fullFunctionName(scc.targetName, scc.targetSignature)); - if (removedFunction != null) { - cluster.executor.submit( - new Runnable() { - @Override - public void run() { - cluster.metadata.triggerOnFunctionRemoved(removedFunction); - } - }); - } - break; - case AGGREGATE: - final AggregateMetadata removedAggregate = - keyspace.removeAggregate( - Metadata.fullFunctionName(scc.targetName, scc.targetSignature)); - if (removedAggregate != null) { - cluster.executor.submit( - new Runnable() { - @Override - public void run() { - cluster.metadata.triggerOnAggregateRemoved(removedAggregate); - } - }); - } - break; - } - } - } - session.cluster.manager.waitForSchemaAgreementAndSignal(connection, this, rs); - break; - default: - logger.info("Ignoring unknown schema change result"); - break; - } - } - break; - default: - set(ArrayBackedResultSet.fromMessage(rm, session, protocolVersion, info, statement)); - break; - } - break; - case ERROR: - setException(((Responses.Error) response).asException(connection.endPoint)); - break; - default: - // This mean we have probably have a bad node, so defunct the connection - connection.defunct( - new ConnectionException( - connection.endPoint, String.format("Got unexpected %s response", response.type))); - setException( - new DriverInternalError( - String.format( - "Got unexpected %s response from %s", response.type, connection.endPoint))); - break; - } - } catch (Throwable e) { - // If we get a bug here, the client will not get it, so better forwarding the error - setException( - new DriverInternalError( - "Unexpected error while processing response from " + connection.endPoint, e)); - } - } - - @Override - public void onSet( - Connection connection, Message.Response response, long latency, int retryCount) { - // This is only called for internal calls (i.e, when the callback is not wrapped in - // ResponseHandler), - // so don't bother with ExecutionInfo. - onSet(connection, response, null, null, latency); - } - - @Override - public void onException( - Connection connection, Exception exception, long latency, int retryCount) { - setException(exception); - } - - @Override - public boolean onTimeout(Connection connection, long latency, int retryCount) { - // This is only called for internal calls (i.e, when the future is not wrapped in - // RequestHandler). - // So just set an exception for the final result, which should be handled correctly by said - // internal call. - setException(new OperationTimedOutException(connection.endPoint)); - return true; - } - - // We sometimes need (in the driver) to set the future from outside this class, - // but AbstractFuture#set is protected so this method. We don't want it public - // however, no particular reason to give users rope to hang themselves. - void setResult(ResultSet rs) { - set(rs); - } - - /** - * Waits for the query to return and return its result. - * - *

This method is usually more convenient than {@link #get} because it: - * - *

    - *
  • Waits for the result uninterruptibly, and so doesn't throw {@link InterruptedException}. - *
  • Returns meaningful exceptions, instead of having to deal with ExecutionException. - *
- * - * As such, it is the preferred way to get the future result. - * - * @throws NoHostAvailableException if no host in the cluster can be contacted successfully to - * execute this query. - * @throws QueryExecutionException if the query triggered an execution exception, that is an - * exception thrown by Cassandra when it cannot execute the query with the requested - * consistency level successfully. - * @throws QueryValidationException if the query is invalid (syntax error, unauthorized or any - * other validation problem). - */ - @Override - public ResultSet getUninterruptibly() { - try { - return Uninterruptibles.getUninterruptibly(this); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } - - /** - * Waits for the provided time for the query to return and return its result if available. - * - *

This method is usually more convenient than {@link #get} because it: - * - *

    - *
  • Waits for the result uninterruptibly, and so doesn't throw {@link InterruptedException}. - *
  • Returns meaningful exceptions, instead of having to deal with ExecutionException. - *
- * - * As such, it is the preferred way to get the future result. - * - * @throws NoHostAvailableException if no host in the cluster can be contacted successfully to - * execute this query. - * @throws QueryExecutionException if the query triggered an execution exception, that is an - * exception thrown by Cassandra when it cannot execute the query with the requested - * consistency level successfully. - * @throws QueryValidationException if the query if invalid (syntax error, unauthorized or any - * other validation problem). - * @throws TimeoutException if the wait timed out (Note that this is different from a Cassandra - * timeout, which is a {@code QueryExecutionException}). - */ - @Override - public ResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException { - try { - return Uninterruptibles.getUninterruptibly(this, timeout, unit); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } - - /** - * Attempts to cancel the execution of the request corresponding to this - * future. This attempt will fail if the request has already returned. - *

- * Please note that this only cancels the request driver side, but nothing - * is done to interrupt the execution of the request Cassandra side (and that even - * if {@code mayInterruptIfRunning} is true) since Cassandra does not - * support such interruption. - *

- * This method can be used to ensure no more work is performed driver side - * (which, while it doesn't include stopping a request already submitted - * to a Cassandra node, may include not retrying another Cassandra host on - * failure/timeout) if the ResultSet is not going to be retried. Typically, - * the code to wait for a request result for a maximum of 1 second could - * look like: - *

-   *   ResultSetFuture future = session.executeAsync(...some query...);
-   *   try {
-   *       ResultSet result = future.get(1, TimeUnit.SECONDS);
-   *       ... process result ...
-   *   } catch (TimeoutException e) {
-   *       future.cancel(true); // Ensure any resource used by this query driver
-   *                            // side is released immediately
-   *       ... handle timeout ...
-   *   }
-   * 
-   *
-   * @param mayInterruptIfRunning the value of this parameter is currently
-   *                              ignored.
-   * @return {@code false} if the future could not be cancelled (it has already
-   * completed normally); {@code true} otherwise.
-   */
-  @Override
-  public boolean cancel(boolean mayInterruptIfRunning) {
-    if (!super.cancel(mayInterruptIfRunning)) return false;
-
-    if (handler != null) {
-      handler.cancel();
-    }
-    return true;
-  }
-
-  @Override
-  public int retryCount() {
-    // This is only called for internal calls (i.e, when the future is not wrapped in
-    // RequestHandler).
-    // There is no retry logic in that case, so the value does not really matter.
-    return 0;
-  }
-}
diff --git a/driver-core/src/main/java/com/datastax/driver/core/DelegatingCluster.java b/driver-core/src/main/java/com/datastax/driver/core/DelegatingCluster.java
deleted file mode 100644
index ad718645e81..00000000000
--- a/driver-core/src/main/java/com/datastax/driver/core/DelegatingCluster.java
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * Copyright DataStax, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.datastax.driver.core;
-
-import com.google.common.util.concurrent.ListenableFuture;
-import java.util.Collections;
-
-/**
- * Base class for custom {@link Cluster} implementations that wrap another instance (delegate /
- * decorator pattern).
- */
-public abstract class DelegatingCluster extends Cluster {
-  /** Builds a new instance. */
-  protected DelegatingCluster() {
-    // Implementation notes:
-    // If Cluster was an interface, delegates would be trivial to write. But, for historical
-    // reasons, it's a class,
-    // and changing that would break backward compatibility. That makes delegates rather convoluted
-    // and error-prone
-    // to write, so we provide DelegatingCluster to abstract the details.
-    // This class ensures that:
-    // - init() is never called on the parent class, because that would initialize the
-    // Cluster.Manager instance and
-    //   create a lot of internal state (thread pools, etc.) that we don't need, since another
-    // Cluster instance is
-    //   already handling the calls.
-    // - all public methods are properly forwarded to the delegate (otherwise they would call the
-    // parent class and
-    //   return inconsistent results).
-    // These two goals are closely related, since a lot of public methods call init(), so
-    // accidentally calling a
-    // parent method could initialize the parent state.
-
-    // Construct parent class with dummy parameters that will never get used (since super.init() is
-    // never called).
-    super("delegating_cluster", Collections.emptyList(), null);
-
-    // Immediately close the parent class's internal Manager, to make sure that it will fail fast if
-    // it's ever
-    // accidentally invoked.
-    super.closeAsync();
-  }
-
-  /**
-   * Returns the delegate instance where all calls will be forwarded.
-   *
-   * @return the delegate.
-   */
-  protected abstract Cluster delegate();
-
-  @Override
-  public Cluster init() {
-    return delegate().init();
-  }
-
-  @Override
-  public Session newSession() {
-    return delegate().newSession();
-  }
-
-  @Override
-  public Session connect() {
-    return delegate().connect();
-  }
-
-  @Override
-  public Session connect(String keyspace) {
-    return delegate().connect(keyspace);
-  }
-
-  @Override
-  public ListenableFuture connectAsync() {
-    return delegate().connectAsync();
-  }
-
-  @Override
-  public ListenableFuture connectAsync(String keyspace) {
-    return delegate().connectAsync(keyspace);
-  }
-
-  @Override
-  public Metadata getMetadata() {
-    return delegate().getMetadata();
-  }
-
-  @Override
-  public Configuration getConfiguration() {
-    return delegate().getConfiguration();
-  }
-
-  @Override
-  public Metrics getMetrics() {
-    return delegate().getMetrics();
-  }
-
-  @Override
-  public Cluster register(Host.StateListener listener) {
-    return delegate().register(listener);
-  }
-
-  @Override
-  public Cluster unregister(Host.StateListener listener) {
-    return delegate().unregister(listener);
-  }
-
-  @Override
-  public Cluster register(LatencyTracker tracker) {
-    return delegate().register(tracker);
-  }
-
-  @Override
-  public Cluster unregister(LatencyTracker tracker) {
-    return delegate().unregister(tracker);
-  }
-
-  @Override
-  public Cluster register(SchemaChangeListener listener) {
-    return delegate().register(listener);
-  }
-
-  @Override
-  public Cluster unregister(SchemaChangeListener listener) {
-    return delegate().unregister(listener);
-  }
-
-  @Override
-  public CloseFuture closeAsync() {
-    return delegate().closeAsync();
-  }
-
-  @Override
-  public void close() {
-    delegate().close();
-  }
-
-  @Override
-  public boolean isClosed() {
-    return delegate().isClosed();
-  }
-}
diff --git a/driver-core/src/main/java/com/datastax/driver/core/DirectedGraph.java b/driver-core/src/main/java/com/datastax/driver/core/DirectedGraph.java
deleted file mode 100644
index 81265bc7046..00000000000
--- a/driver-core/src/main/java/com/datastax/driver/core/DirectedGraph.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright DataStax, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.datastax.driver.core;
-
-import com.datastax.driver.core.exceptions.DriverInternalError;
-import com.google.common.base.Preconditions;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Queue;
-
-/** A basic directed graph implementation to perform topological sorts. */
-class DirectedGraph {
-
-  // We need to keep track of the predecessor count. For simplicity, use a map to store it alongside
-  // the vertices.
-  final Map vertices;
-  final Multimap adjacencyList;
-  boolean wasSorted;
-  final Comparator comparator;
-
-  DirectedGraph(Comparator comparator, List vertices) {
-    this.comparator = comparator;
-    this.vertices = Maps.newHashMapWithExpectedSize(vertices.size());
-    this.adjacencyList = HashMultimap.create();
-
-    for (V vertex : vertices) {
-      this.vertices.put(vertex, 0);
-    }
-  }
-
-  DirectedGraph(Comparator comparator, V... vertices) {
-    this(comparator, Arrays.asList(vertices));
-  }
-
-  /**
-   * this assumes that {@code from} and {@code to} were part of the vertices passed to the
-   * constructor
-   */
-  void addEdge(V from, V to) {
-    Preconditions.checkArgument(vertices.containsKey(from) && vertices.containsKey(to));
-    adjacencyList.put(from, to);
-    vertices.put(to, vertices.get(to) + 1);
-  }
-
-  /** one-time use only, calling this multiple times on the same graph won't work */
-  List topologicalSort() {
-    Preconditions.checkState(!wasSorted);
-    wasSorted = true;
-
-    Queue queue = new LinkedList();
-
-    // Sort vertices so order of evaluation is always the same (instead of depending on undefined
-    // map order behavior)
-    List orderedVertices = new ArrayList(vertices.keySet());
-    Collections.sort(orderedVertices, comparator);
-    for (V v : orderedVertices) {
-      if (vertices.get(v) == 0) queue.add(v);
-    }
-
-    List result = Lists.newArrayList();
-    while (!queue.isEmpty()) {
-      V vertex = queue.remove();
-      result.add(vertex);
-      List adjacentVertices = new ArrayList(adjacencyList.get(vertex));
-      Collections.sort(adjacentVertices, comparator);
-      for (V successor : adjacentVertices) {
-        if (decrementAndGetCount(successor) == 0) queue.add(successor);
-      }
-    }
-
-    if (result.size() != vertices.size())
-      throw new DriverInternalError("failed to perform topological sort, graph has a cycle");
-
-    return result;
-  }
-
-  private int decrementAndGetCount(V vertex) {
-    Integer count = vertices.get(vertex);
-    count = count - 1;
-    vertices.put(vertex, count);
-    return count;
-  }
-}
diff --git a/driver-core/src/main/java/com/datastax/driver/core/DriverThrowables.java b/driver-core/src/main/java/com/datastax/driver/core/DriverThrowables.java
deleted file mode 100644
index 3a3898e2a6d..00000000000
--- a/driver-core/src/main/java/com/datastax/driver/core/DriverThrowables.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright DataStax, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.datastax.driver.core;
-
-import com.datastax.driver.core.exceptions.DriverException;
-import com.datastax.driver.core.exceptions.DriverInternalError;
-import java.util.concurrent.ExecutionException;
-
-class DriverThrowables {
-
-  static RuntimeException propagateCause(ExecutionException e) {
-    Throwable cause = e.getCause();
-
-    if (cause instanceof Error) throw ((Error) cause);
-
-    // We could just rethrow e.getCause(). However, the cause of the ExecutionException has likely
-    // been
-    // created on the I/O thread receiving the response. Which means that the stacktrace associated
-    // with said cause will make no mention of the current thread. This is painful for say, finding
-    // out which execute() statement actually raised the exception. So instead, we re-create the
-    // exception.
-    if (cause instanceof DriverException) throw ((DriverException) cause).copy();
-    else throw new DriverInternalError("Unexpected exception thrown", cause);
-  }
-}
diff --git a/driver-core/src/main/java/com/datastax/driver/core/Duration.java b/driver-core/src/main/java/com/datastax/driver/core/Duration.java
deleted file mode 100644
index 530de022af8..00000000000
--- a/driver-core/src/main/java/com/datastax/driver/core/Duration.java
+++ /dev/null
@@ -1,570 +0,0 @@
-/*
- * Copyright DataStax, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.datastax.driver.core;
-
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.common.base.Objects;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * Represents a duration. A duration stores separately months, days, and seconds due to the fact
- * that the number of days in a month varies, and a day can have 23 or 25 hours if a daylight saving
- * is involved.
- */
-public final class Duration {
-
-  static final long NANOS_PER_MICRO = 1000L;
-  static final long NANOS_PER_MILLI = 1000 * NANOS_PER_MICRO;
-  static final long NANOS_PER_SECOND = 1000 * NANOS_PER_MILLI;
-  static final long NANOS_PER_MINUTE = 60 * NANOS_PER_SECOND;
-  static final long NANOS_PER_HOUR = 60 * NANOS_PER_MINUTE;
-  static final int DAYS_PER_WEEK = 7;
-  static final int MONTHS_PER_YEAR = 12;
-
-  /** The Regexp used to parse the duration provided as String. */
-  private static final Pattern STANDARD_PATTERN =
-      Pattern.compile(
-          "\\G(\\d+)(y|Y|mo|MO|mO|Mo|w|W|d|D|h|H|s|S|ms|MS|mS|Ms|us|US|uS|Us|µs|µS|ns|NS|nS|Ns|m|M)");
-
-  /**
-   * The Regexp used to parse the duration when provided in the ISO 8601 format with designators.
-   */
-  private static final Pattern ISO8601_PATTERN =
-      Pattern.compile("P((\\d+)Y)?((\\d+)M)?((\\d+)D)?(T((\\d+)H)?((\\d+)M)?((\\d+)S)?)?");
-
-  /**
-   * The Regexp used to parse the duration when provided in the ISO 8601 format with designators.
-   */
-  private static final Pattern ISO8601_WEEK_PATTERN = Pattern.compile("P(\\d+)W");
-
-  /** The Regexp used to parse the duration when provided in the ISO 8601 alternative format. */
-  private static final Pattern ISO8601_ALTERNATIVE_PATTERN =
-      Pattern.compile("P(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})");
-
-  /** The number of months. */
-  private final int months;
-
-  /** The number of days. */
-  private final int days;
-
-  /** The number of nanoseconds. */
-  private final long nanoseconds;
-
-  private Duration(int months, int days, long nanoseconds) {
-    // Makes sure that all the values are negative if one of them is
-    if ((months < 0 || days < 0 || nanoseconds < 0)
-        && ((months > 0 || days > 0 || nanoseconds > 0))) {
-      throw new IllegalArgumentException(
-          String.format(
-              "All values must be either negative or positive, got %d months, %d days, %d nanoseconds",
-              months, days, nanoseconds));
-    }
-    this.months = months;
-    this.days = days;
-    this.nanoseconds = nanoseconds;
-  }
-
-  /**
-   * Creates a duration with the given number of months, days and nanoseconds.
-   *
-   * 

A duration can be negative. In this case, all the non zero values must be negative. - * - * @param months the number of months - * @param days the number of days - * @param nanoseconds the number of nanoseconds - * @throws IllegalArgumentException if the values are not all negative or all positive - */ - public static Duration newInstance(int months, int days, long nanoseconds) { - return new Duration(months, days, nanoseconds); - } - - /** - * Converts a String into a duration. - * - *

The accepted formats are: - * - *

    - *
  • multiple digits followed by a time unit like: 12h30m where the time unit can be: - *
      - *
    • {@code y}: years - *
    • {@code m}: months - *
    • {@code w}: weeks - *
    • {@code d}: days - *
    • {@code h}: hours - *
    • {@code m}: minutes - *
    • {@code s}: seconds - *
    • {@code ms}: milliseconds - *
    • {@code us} or {@code µs}: microseconds - *
    • {@code ns}: nanoseconds - *
    - *
  • ISO 8601 format: P[n]Y[n]M[n]DT[n]H[n]M[n]S or P[n]W - *
  • ISO 8601 alternative format: P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss] - *
- * - * @param input the String to convert - * @return a {@link Duration} - */ - public static Duration from(String input) { - boolean isNegative = input.startsWith("-"); - String source = isNegative ? input.substring(1) : input; - - if (source.startsWith("P")) { - if (source.endsWith("W")) return parseIso8601WeekFormat(isNegative, source); - - if (source.contains("-")) return parseIso8601AlternativeFormat(isNegative, source); - - return parseIso8601Format(isNegative, source); - } - return parseStandardFormat(isNegative, source); - } - - private static Duration parseIso8601Format(boolean isNegative, String source) { - Matcher matcher = ISO8601_PATTERN.matcher(source); - if (!matcher.matches()) - throw new IllegalArgumentException( - String.format("Unable to convert '%s' to a duration", source)); - - Builder builder = new Builder(isNegative); - if (matcher.group(1) != null) builder.addYears(groupAsLong(matcher, 2)); - - if (matcher.group(3) != null) builder.addMonths(groupAsLong(matcher, 4)); - - if (matcher.group(5) != null) builder.addDays(groupAsLong(matcher, 6)); - - // Checks if the String contains time information - if (matcher.group(7) != null) { - if (matcher.group(8) != null) builder.addHours(groupAsLong(matcher, 9)); - - if (matcher.group(10) != null) builder.addMinutes(groupAsLong(matcher, 11)); - - if (matcher.group(12) != null) builder.addSeconds(groupAsLong(matcher, 13)); - } - return builder.build(); - } - - private static Duration parseIso8601AlternativeFormat(boolean isNegative, String source) { - Matcher matcher = ISO8601_ALTERNATIVE_PATTERN.matcher(source); - if (!matcher.matches()) - throw new IllegalArgumentException( - String.format("Unable to convert '%s' to a duration", source)); - - return new Builder(isNegative) - .addYears(groupAsLong(matcher, 1)) - .addMonths(groupAsLong(matcher, 2)) - .addDays(groupAsLong(matcher, 3)) - .addHours(groupAsLong(matcher, 4)) - .addMinutes(groupAsLong(matcher, 5)) - .addSeconds(groupAsLong(matcher, 6)) - .build(); - } - - private static Duration parseIso8601WeekFormat(boolean isNegative, String source) { - Matcher matcher = ISO8601_WEEK_PATTERN.matcher(source); - if (!matcher.matches()) - throw new IllegalArgumentException( - String.format("Unable to convert '%s' to a duration", source)); - - return new Builder(isNegative).addWeeks(groupAsLong(matcher, 1)).build(); - } - - private static Duration parseStandardFormat(boolean isNegative, String source) { - Matcher matcher = STANDARD_PATTERN.matcher(source); - if (!matcher.find()) - throw new IllegalArgumentException( - String.format("Unable to convert '%s' to a duration", source)); - - Builder builder = new Builder(isNegative); - boolean done; - - do { - long number = groupAsLong(matcher, 1); - String symbol = matcher.group(2); - add(builder, number, symbol); - done = matcher.end() == source.length(); - } while (matcher.find()); - - if (!done) - throw new IllegalArgumentException( - String.format("Unable to convert '%s' to a duration", source)); - - return builder.build(); - } - - private static long groupAsLong(Matcher matcher, int group) { - return Long.parseLong(matcher.group(group)); - } - - private static Builder add(Builder builder, long number, String symbol) { - String s = symbol.toLowerCase(); - if (s.equals("y")) { - return builder.addYears(number); - } else if (s.equals("mo")) { - return builder.addMonths(number); - } else if (s.equals("w")) { - return builder.addWeeks(number); - } else if (s.equals("d")) { - return builder.addDays(number); - } else if (s.equals("h")) { - return builder.addHours(number); - } else if (s.equals("m")) { - return builder.addMinutes(number); - } else if (s.equals("s")) { - return builder.addSeconds(number); - } else if (s.equals("ms")) { - return builder.addMillis(number); - } else if (s.equals("us") || s.equals("µs")) { - return builder.addMicros(number); - } else if (s.equals("ns")) { - return builder.addNanos(number); - } - throw new IllegalArgumentException(String.format("Unknown duration symbol '%s'", symbol)); - } - - /** - * Appends the result of the division to the specified builder if the dividend is not zero. - * - * @param builder the builder to append to - * @param dividend the dividend - * @param divisor the divisor - * @param unit the time unit to append after the result of the division - * @return the remainder of the division - */ - private static long append(StringBuilder builder, long dividend, long divisor, String unit) { - if (dividend == 0 || dividend < divisor) return dividend; - - builder.append(dividend / divisor).append(unit); - return dividend % divisor; - } - - /** - * Returns the number of months in this duration. - * - * @return the number of months in this duration. - */ - public int getMonths() { - return months; - } - - /** - * Returns the number of days in this duration. - * - * @return the number of days in this duration. - */ - public int getDays() { - return days; - } - - /** - * Returns the number of nanoseconds in this duration. - * - * @return the number of months in this duration. - */ - public long getNanoseconds() { - return nanoseconds; - } - - @Override - public int hashCode() { - return Objects.hashCode(days, months, nanoseconds); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Duration)) return false; - - Duration other = (Duration) obj; - return days == other.days && months == other.months && nanoseconds == other.nanoseconds; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - - if (months < 0 || days < 0 || nanoseconds < 0) builder.append('-'); - - long remainder = append(builder, Math.abs(months), MONTHS_PER_YEAR, "y"); - append(builder, remainder, 1, "mo"); - - append(builder, Math.abs(days), 1, "d"); - - if (nanoseconds != 0) { - remainder = append(builder, Math.abs(nanoseconds), NANOS_PER_HOUR, "h"); - remainder = append(builder, remainder, NANOS_PER_MINUTE, "m"); - remainder = append(builder, remainder, NANOS_PER_SECOND, "s"); - remainder = append(builder, remainder, NANOS_PER_MILLI, "ms"); - remainder = append(builder, remainder, NANOS_PER_MICRO, "us"); - append(builder, remainder, 1, "ns"); - } - return builder.toString(); - } - - private static class Builder { - /** {@code true} if the duration is a negative one, {@code false} otherwise. */ - private final boolean isNegative; - - /** The number of months. */ - private int months; - - /** The number of days. */ - private int days; - - /** The number of nanoseconds. */ - private long nanoseconds; - - /** We need to make sure that the values for each units are provided in order. */ - private int currentUnitIndex; - - public Builder(boolean isNegative) { - this.isNegative = isNegative; - } - - /** - * Adds the specified amount of years. - * - * @param numberOfYears the number of years to add. - * @return this {@code Builder} - */ - public Builder addYears(long numberOfYears) { - validateOrder(1); - validateMonths(numberOfYears, MONTHS_PER_YEAR); - months += numberOfYears * MONTHS_PER_YEAR; - return this; - } - - /** - * Adds the specified amount of months. - * - * @param numberOfMonths the number of months to add. - * @return this {@code Builder} - */ - public Builder addMonths(long numberOfMonths) { - validateOrder(2); - validateMonths(numberOfMonths, 1); - months += numberOfMonths; - return this; - } - - /** - * Adds the specified amount of weeks. - * - * @param numberOfWeeks the number of weeks to add. - * @return this {@code Builder} - */ - public Builder addWeeks(long numberOfWeeks) { - validateOrder(3); - validateDays(numberOfWeeks, DAYS_PER_WEEK); - days += numberOfWeeks * DAYS_PER_WEEK; - return this; - } - - /** - * Adds the specified amount of days. - * - * @param numberOfDays the number of days to add. - * @return this {@code Builder} - */ - public Builder addDays(long numberOfDays) { - validateOrder(4); - validateDays(numberOfDays, 1); - days += numberOfDays; - return this; - } - - /** - * Adds the specified amount of hours. - * - * @param numberOfHours the number of hours to add. - * @return this {@code Builder} - */ - public Builder addHours(long numberOfHours) { - validateOrder(5); - validateNanos(numberOfHours, NANOS_PER_HOUR); - nanoseconds += numberOfHours * NANOS_PER_HOUR; - return this; - } - - /** - * Adds the specified amount of minutes. - * - * @param numberOfMinutes the number of minutes to add. - * @return this {@code Builder} - */ - public Builder addMinutes(long numberOfMinutes) { - validateOrder(6); - validateNanos(numberOfMinutes, NANOS_PER_MINUTE); - nanoseconds += numberOfMinutes * NANOS_PER_MINUTE; - return this; - } - - /** - * Adds the specified amount of seconds. - * - * @param numberOfSeconds the number of seconds to add. - * @return this {@code Builder} - */ - public Builder addSeconds(long numberOfSeconds) { - validateOrder(7); - validateNanos(numberOfSeconds, NANOS_PER_SECOND); - nanoseconds += numberOfSeconds * NANOS_PER_SECOND; - return this; - } - - /** - * Adds the specified amount of milliseconds. - * - * @param numberOfMillis the number of milliseconds to add. - * @return this {@code Builder} - */ - public Builder addMillis(long numberOfMillis) { - validateOrder(8); - validateNanos(numberOfMillis, NANOS_PER_MILLI); - nanoseconds += numberOfMillis * NANOS_PER_MILLI; - return this; - } - - /** - * Adds the specified amount of microseconds. - * - * @param numberOfMicros the number of microseconds to add. - * @return this {@code Builder} - */ - public Builder addMicros(long numberOfMicros) { - validateOrder(9); - validateNanos(numberOfMicros, NANOS_PER_MICRO); - nanoseconds += numberOfMicros * NANOS_PER_MICRO; - return this; - } - - /** - * Adds the specified amount of nanoseconds. - * - * @param numberOfNanos the number of nanoseconds to add. - * @return this {@code Builder} - */ - public Builder addNanos(long numberOfNanos) { - validateOrder(10); - validateNanos(numberOfNanos, 1); - nanoseconds += numberOfNanos; - return this; - } - - /** - * Validates that the total number of months can be stored. - * - * @param units the number of units that need to be added - * @param monthsPerUnit the number of days per unit - */ - private void validateMonths(long units, int monthsPerUnit) { - validate(units, (Integer.MAX_VALUE - months) / monthsPerUnit, "months"); - } - - /** - * Validates that the total number of days can be stored. - * - * @param units the number of units that need to be added - * @param daysPerUnit the number of days per unit - */ - private void validateDays(long units, int daysPerUnit) { - validate(units, (Integer.MAX_VALUE - days) / daysPerUnit, "days"); - } - - /** - * Validates that the total number of nanoseconds can be stored. - * - * @param units the number of units that need to be added - * @param nanosPerUnit the number of nanoseconds per unit - */ - private void validateNanos(long units, long nanosPerUnit) { - validate(units, (Long.MAX_VALUE - nanoseconds) / nanosPerUnit, "nanoseconds"); - } - - /** - * Validates that the specified amount is less than the limit. - * - * @param units the number of units to check - * @param limit the limit on the number of units - * @param unitName the unit name - */ - private void validate(long units, long limit, String unitName) { - checkArgument( - units <= limit, - "Invalid duration. The total number of %s must be less or equal to %s", - unitName, - Integer.MAX_VALUE); - } - - /** - * Validates that the duration values are added in the proper order. - * - * @param unitIndex the unit index (e.g. years=1, months=2, ...) - */ - private void validateOrder(int unitIndex) { - if (unitIndex == currentUnitIndex) - throw new IllegalArgumentException( - String.format( - "Invalid duration. The %s are specified multiple times", getUnitName(unitIndex))); - - if (unitIndex <= currentUnitIndex) - throw new IllegalArgumentException( - String.format( - "Invalid duration. The %s should be after %s", - getUnitName(currentUnitIndex), getUnitName(unitIndex))); - - currentUnitIndex = unitIndex; - } - - /** - * Returns the name of the unit corresponding to the specified index. - * - * @param unitIndex the unit index - * @return the name of the unit corresponding to the specified index. - */ - private String getUnitName(int unitIndex) { - switch (unitIndex) { - case 1: - return "years"; - case 2: - return "months"; - case 3: - return "weeks"; - case 4: - return "days"; - case 5: - return "hours"; - case 6: - return "minutes"; - case 7: - return "seconds"; - case 8: - return "milliseconds"; - case 9: - return "microseconds"; - case 10: - return "nanoseconds"; - default: - throw new AssertionError("unknown unit index: " + unitIndex); - } - } - - public Duration build() { - return isNegative - ? new Duration(-months, -days, -nanoseconds) - : new Duration(months, days, nanoseconds); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/EndPoint.java b/driver-core/src/main/java/com/datastax/driver/core/EndPoint.java deleted file mode 100644 index 80eefbce103..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/EndPoint.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.net.InetSocketAddress; - -/** Encapsulates the information needed by the driver to open connections to a node. */ -public interface EndPoint { - - /** - * Resolves this instance to a socket address. - * - *

This will be called each time the driver opens a new connection to the node. The returned - * address cannot be null. - */ - InetSocketAddress resolve(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/EndPointFactory.java b/driver-core/src/main/java/com/datastax/driver/core/EndPointFactory.java deleted file mode 100644 index 4ef0e4fa1da..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/EndPointFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Produces {@link EndPoint} instances representing the connection information to every node. - * - *

This component is reserved for advanced use cases where the driver needs more than an IP - * address to connect. - * - *

Note that if endpoints do not translate to addresses 1-to-1, the auth provider and SSL options - * should be instances of {@link ExtendedAuthProvider} and {@link - * ExtendedRemoteEndpointAwareSslOptions} respectively. - */ -public interface EndPointFactory { - - void init(Cluster cluster); - - /** - * Creates an instance from a row in {@code system.peers}, or returns {@code null} if there is no - * sufficient information. - */ - EndPoint create(Row peersRow); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/EventDebouncer.java b/driver-core/src/main/java/com/datastax/driver/core/EventDebouncer.java deleted file mode 100644 index fc6cf90a71c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/EventDebouncer.java +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.concurrent.TimeUnit.NANOSECONDS; -import static java.util.concurrent.TimeUnit.SECONDS; - -import com.datastax.driver.core.utils.MoreFutures; -import com.google.common.collect.Lists; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A helper class to debounce events received by the Control Connection. - * - *

This class accumulates received events, and delivers them when either: - no events have been - * received for delayMs - maxPendingEvents have been received - */ -abstract class EventDebouncer { - - private static final Logger logger = LoggerFactory.getLogger(EventDebouncer.class); - - private static final int DEFAULT_MAX_QUEUED_EVENTS = 10000; - - private final String name; - - private final AtomicReference immediateDelivery = - new AtomicReference(null); - private final AtomicReference delayedDelivery = - new AtomicReference(null); - - private final ScheduledExecutorService executor; - - private final DeliveryCallback callback; - - private final int maxQueuedEvents; - - private final Queue> events; - private final AtomicInteger eventCount; - - private enum State { - NEW, - RUNNING, - STOPPED - } - - private volatile State state; - - private static final long OVERFLOW_WARNING_INTERVAL = NANOSECONDS.convert(5, SECONDS); - private volatile long lastOverflowWarning = Long.MIN_VALUE; - - EventDebouncer(String name, ScheduledExecutorService executor, DeliveryCallback callback) { - this(name, executor, callback, DEFAULT_MAX_QUEUED_EVENTS); - } - - EventDebouncer( - String name, - ScheduledExecutorService executor, - DeliveryCallback callback, - int maxQueuedEvents) { - this.name = name; - this.executor = executor; - this.callback = callback; - this.maxQueuedEvents = maxQueuedEvents; - this.events = new ConcurrentLinkedQueue>(); - this.eventCount = new AtomicInteger(); - this.state = State.NEW; - } - - abstract int maxPendingEvents(); - - abstract long delayMs(); - - void start() { - logger.trace("Starting {} debouncer...", name); - state = State.RUNNING; - if (!events.isEmpty()) { - logger.trace( - "{} debouncer: {} events were accumulated before the debouncer started: delivering now", - name, - eventCount.get()); - scheduleImmediateDelivery(); - } - } - - void stop() { - logger.trace("Stopping {} debouncer...", name); - state = State.STOPPED; - while (true) { - DeliveryAttempt previous = cancelDelayedDelivery(); - if (delayedDelivery.compareAndSet(previous, null)) { - break; - } - } - while (true) { - DeliveryAttempt previous = cancelImmediateDelivery(); - if (immediateDelivery.compareAndSet(previous, null)) { - break; - } - } - - completeAllPendingFutures(); - - logger.trace("{} debouncer stopped", name); - } - - private void completeAllPendingFutures() { - Entry entry; - while ((entry = this.events.poll()) != null) { - entry.future.set(null); - } - } - - /** @return a future that will complete once the event has been processed */ - ListenableFuture eventReceived(T event) { - if (state == State.STOPPED) { - logger.trace("{} debouncer is stopped, rejecting event: {}", name, event); - return MoreFutures.VOID_SUCCESS; - } - checkNotNull(event); - logger.trace("{} debouncer: event received {}", name, event); - - // Safeguard against the queue filling up faster than we can process it - if (eventCount.incrementAndGet() > maxQueuedEvents) { - long now = System.nanoTime(); - if (now > lastOverflowWarning + OVERFLOW_WARNING_INTERVAL) { - lastOverflowWarning = now; - logger.warn( - "{} debouncer enqueued more than {} events, rejecting new events. " - + "This should not happen and is likely a sign that something is wrong.", - name, - maxQueuedEvents); - } - eventCount.decrementAndGet(); - return MoreFutures.VOID_SUCCESS; - } - - Entry entry = new Entry(event); - try { - events.add(entry); - } catch (RuntimeException e) { - eventCount.decrementAndGet(); - throw e; - } - - if (state == State.RUNNING) { - int count = eventCount.get(); - int maxPendingEvents = maxPendingEvents(); - if (count < maxPendingEvents) { - scheduleDelayedDelivery(); - } else if (count == maxPendingEvents) { - scheduleImmediateDelivery(); - } - } else if (state == State.STOPPED) { - // If we race with stop() since the check at the beginning, ensure the future - // gets completed (no-op if the future was already set). - entry.future.set(null); - } - return entry.future; - } - - void scheduleImmediateDelivery() { - cancelDelayedDelivery(); - - while (state == State.RUNNING) { - DeliveryAttempt previous = immediateDelivery.get(); - if (previous != null) previous.cancel(); - - DeliveryAttempt current = new DeliveryAttempt(); - if (immediateDelivery.compareAndSet(previous, current)) { - current.executeNow(); - return; - } - } - } - - private void scheduleDelayedDelivery() { - while (state == State.RUNNING) { - DeliveryAttempt previous = cancelDelayedDelivery(); - DeliveryAttempt next = new DeliveryAttempt(); - if (delayedDelivery.compareAndSet(previous, next)) { - next.scheduleAfterDelay(); - break; - } - } - } - - private DeliveryAttempt cancelDelayedDelivery() { - return cancelDelivery(delayedDelivery.get()); - } - - private DeliveryAttempt cancelImmediateDelivery() { - return cancelDelivery(immediateDelivery.get()); - } - - private DeliveryAttempt cancelDelivery(DeliveryAttempt previous) { - if (previous != null) { - previous.cancel(); - } - return previous; - } - - private void deliverEvents() { - if (state == State.STOPPED) { - completeAllPendingFutures(); - return; - } - final List toDeliver = Lists.newArrayList(); - final List> futures = Lists.newArrayList(); - - Entry entry; - // Limit the number of events we dequeue, to avoid an infinite loop if the queue starts filling - // faster than we can consume it. - int count = 0; - while (++count <= maxQueuedEvents && (entry = this.events.poll()) != null) { - toDeliver.add(entry.event); - futures.add(entry.future); - } - eventCount.addAndGet(-toDeliver.size()); - - if (toDeliver.isEmpty()) { - logger.trace("{} debouncer: no events to deliver", name); - } else { - logger.trace("{} debouncer: delivering {} events", name, toDeliver.size()); - ListenableFuture delivered = callback.deliver(toDeliver); - GuavaCompatibility.INSTANCE.addCallback( - delivered, - new FutureCallback() { - @Override - public void onSuccess(Object result) { - for (SettableFuture future : futures) future.set(null); - } - - @Override - public void onFailure(Throwable t) { - for (SettableFuture future : futures) future.setException(t); - } - }); - } - - // If we didn't dequeue all events (or new ones arrived since we did), make sure we eventually - // process the remaining events, because eventReceived might have skipped the delivery - if (eventCount.get() > 0) scheduleDelayedDelivery(); - } - - class DeliveryAttempt extends ExceptionCatchingRunnable { - - volatile Future deliveryFuture; - - boolean isDone() { - return deliveryFuture != null && deliveryFuture.isDone(); - } - - void cancel() { - if (deliveryFuture != null) deliveryFuture.cancel(true); - } - - void executeNow() { - if (state != State.STOPPED) deliveryFuture = executor.submit(this); - } - - void scheduleAfterDelay() { - if (state != State.STOPPED) - deliveryFuture = executor.schedule(this, delayMs(), TimeUnit.MILLISECONDS); - } - - @Override - public void runMayThrow() throws Exception { - deliverEvents(); - } - } - - interface DeliveryCallback { - - /** - * Deliver the given list of events. The given list is a private copy and any modification made - * to it has no side-effect; it is also guaranteed not to be null nor empty. - * - * @param events the events to deliver - */ - ListenableFuture deliver(List events); - } - - static class Entry { - final T event; - final SettableFuture future; - - Entry(T event) { - this.event = event; - this.future = SettableFuture.create(); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ExceptionCatchingRunnable.java b/driver-core/src/main/java/com/datastax/driver/core/ExceptionCatchingRunnable.java deleted file mode 100644 index 345e26d2b17..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ExceptionCatchingRunnable.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -// Simple utility class to make sure we don't let exception slip away and kill -// our executors. -abstract class ExceptionCatchingRunnable implements Runnable { - - private static final Logger logger = LoggerFactory.getLogger(ExceptionCatchingRunnable.class); - - public abstract void runMayThrow() throws Exception; - - @Override - public void run() { - try { - runMayThrow(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (Exception e) { - logger.error("Unexpected error while executing task", e); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ExceptionCode.java b/driver-core/src/main/java/com/datastax/driver/core/ExceptionCode.java deleted file mode 100644 index 4a745da5ff0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ExceptionCode.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import java.util.HashMap; -import java.util.Map; - -/** Exceptions code, as defined by the native protocol. */ -enum ExceptionCode { - SERVER_ERROR(0x0000), - PROTOCOL_ERROR(0x000A), - - BAD_CREDENTIALS(0x0100), - - // 1xx: problem during request execution - UNAVAILABLE(0x1000), - OVERLOADED(0x1001), - IS_BOOTSTRAPPING(0x1002), - TRUNCATE_ERROR(0x1003), - WRITE_TIMEOUT(0x1100), - READ_TIMEOUT(0x1200), - READ_FAILURE(0x1300), - FUNCTION_FAILURE(0x1400), - WRITE_FAILURE(0x1500), - CDC_WRITE_FAILURE(0x1600), - CAS_WRITE_UNKNOWN(0x1700), - - // 2xx: problem validating the request - SYNTAX_ERROR(0x2000), - UNAUTHORIZED(0x2100), - INVALID(0x2200), - CONFIG_ERROR(0x2300), - ALREADY_EXISTS(0x2400), - UNPREPARED(0x2500); - - public final int value; - private static final Map valueToCode = - new HashMap(ExceptionCode.values().length); - - static { - for (ExceptionCode code : ExceptionCode.values()) valueToCode.put(code.value, code); - } - - private ExceptionCode(int value) { - this.value = value; - } - - public static ExceptionCode fromValue(int value) { - ExceptionCode code = valueToCode.get(value); - if (code == null) throw new DriverInternalError(String.format("Unknown error code %d", value)); - return code; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ExecutionInfo.java b/driver-core/src/main/java/com/datastax/driver/core/ExecutionInfo.java deleted file mode 100644 index 5459b0a592c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ExecutionInfo.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.Bytes; -import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** Basic information on the execution of a query. */ -public class ExecutionInfo { - private final int speculativeExecutions; - private final int successfulExecutionIndex; - private final List triedHosts; - private final ConsistencyLevel achievedConsistency; - private final QueryTrace trace; - private final ByteBuffer pagingState; - private final ProtocolVersion protocolVersion; - private final CodecRegistry codecRegistry; - private final Statement statement; - private volatile boolean schemaInAgreement; - private final List warnings; - private final Map incomingPayload; - - private ExecutionInfo( - int speculativeExecutions, - int successfulExecutionIndex, - List triedHosts, - ConsistencyLevel achievedConsistency, - QueryTrace trace, - ByteBuffer pagingState, - ProtocolVersion protocolVersion, - CodecRegistry codecRegistry, - Statement statement, - boolean schemaAgreement, - List warnings, - Map incomingPayload) { - this.speculativeExecutions = speculativeExecutions; - this.successfulExecutionIndex = successfulExecutionIndex; - this.triedHosts = triedHosts; - this.achievedConsistency = achievedConsistency; - this.trace = trace; - this.pagingState = pagingState; - this.protocolVersion = protocolVersion; - this.codecRegistry = codecRegistry; - this.statement = statement; - this.schemaInAgreement = schemaAgreement; - this.warnings = warnings; - this.incomingPayload = incomingPayload; - } - - ExecutionInfo(Host singleHost) { - this( - 0, - 0, - ImmutableList.of(singleHost), - null, - null, - null, - null, - null, - null, - true, - Collections.emptyList(), - null); - } - - public ExecutionInfo( - int speculativeExecutions, - int successfulExecutionIndex, - List triedHosts, - ConsistencyLevel achievedConsistency, - Map customPayload) { - this( - speculativeExecutions, - successfulExecutionIndex, - triedHosts, - achievedConsistency, - null, - null, - null, - null, - null, - false, - null, - customPayload); - } - - ExecutionInfo with( - QueryTrace newTrace, - List newWarnings, - ByteBuffer newPagingState, - Statement newStatement, - ProtocolVersion protocolVersion, - CodecRegistry codecRegistry) { - return new ExecutionInfo( - speculativeExecutions, - successfulExecutionIndex, - triedHosts, - achievedConsistency, - newTrace, - newPagingState, - protocolVersion, - codecRegistry, - newStatement, - schemaInAgreement, - newWarnings, - incomingPayload); - } - - /** - * The list of tried hosts for this query. - * - *

In general, this will be a singleton list with the host that coordinated that query. - * However: - * - *

    - *
  • if a host is tried by the driver but is dead or in error, that host is recorded and the - * query is retried; - *
  • on a timeout or unavailable exception, some {@link - * com.datastax.driver.core.policies.RetryPolicy} may retry the query on the same host, so - * the same host might appear twice. - *
  • if {@link com.datastax.driver.core.policies.SpeculativeExecutionPolicy speculative - * executions} are enabled, this will also contain hosts that were tried by other executions - * (however, note that this only contains hosts which timed out, or for which a response was - * received; if an execution is waiting for a response from a host and another execution - * completes the request in the meantime, then the host of the first execution will not be - * in that list). - *
- * - *

If you are only interested in fetching the final (and often only) node coordinating the - * query, {@link #getQueriedHost} provides a shortcut to fetch the last element of the list - * returned by this method. - * - * @return the list of tried hosts for this query, in the order tried. - */ - public List getTriedHosts() { - return triedHosts; - } - - /** - * Return the Cassandra host that coordinated this query. - * - *

This is a shortcut for {@code getTriedHosts().get(getTriedHosts().size() - 1)}. - * - * @return return the Cassandra host that coordinated this query. - */ - public Host getQueriedHost() { - return triedHosts.get(triedHosts.size() - 1); - } - - /** - * The number of speculative executions that were started for this query. - * - *

This does not include the initial, normal execution of the query. Therefore, if speculative - * executions are disabled, this will always be 0. If they are enabled and one speculative - * execution was triggered in addition to the initial execution, this will be 1, etc. - * - * @see #getSuccessfulExecutionIndex() - * @see - * Cluster.Builder#withSpeculativeExecutionPolicy(com.datastax.driver.core.policies.SpeculativeExecutionPolicy) - */ - public int getSpeculativeExecutions() { - return speculativeExecutions; - } - - /** - * The index of the execution that completed this query. - * - *

0 represents the initial, normal execution of the query, 1 represents the first speculative - * execution, etc. - * - * @see #getSpeculativeExecutions() - * @see - * Cluster.Builder#withSpeculativeExecutionPolicy(com.datastax.driver.core.policies.SpeculativeExecutionPolicy) - */ - public int getSuccessfulExecutionIndex() { - return successfulExecutionIndex; - } - - /** - * If the query returned without achieving the requested consistency level due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, this return the biggest consistency level that - * has been actually achieved by the query. - * - *

Note that the default {@code RetryPolicy} ({@link - * com.datastax.driver.core.policies.DefaultRetryPolicy}) will never allow a query to be - * successful without achieving the initially requested consistency level and hence with that - * default policy, this method will always return {@code null}. However, it might - * occasionally return a non-{@code null} with say, {@link - * com.datastax.driver.core.policies.DowngradingConsistencyRetryPolicy}. - * - * @return {@code null} if the original consistency level of the query was achieved, or the - * consistency level that was ultimately achieved if the {@code RetryPolicy} triggered a retry - * at a different consistency level than the original one. - */ - public ConsistencyLevel getAchievedConsistencyLevel() { - return achievedConsistency; - } - - /** - * Return the query trace if tracing was enabled on this query. - * - *

Note that accessing the fields of the the returned object will trigger a blocking - * background query. - * - * @return the {@code QueryTrace} object for this query if tracing was enable for this query, or - * {@code null} otherwise. - */ - public QueryTrace getQueryTrace() { - return trace; - } - - /** - * Placeholder for async query trace retrieval (not implemented yet). - * - *

Async query trace retrieval will be implemented in a future version. This method is added - * now to avoid breaking binary compatibility later. The current implementation merely wraps the - * result of {@link #getQueryTrace()} in an immediate future; it will still trigger a blocking - * query when the query trace's fields are accessed. - * - * @return currently, an immediate future containing the result of {@link #getQueryTrace()}. - */ - public ListenableFuture getQueryTraceAsync() { - return Futures.immediateFuture(trace); - } - - /** - * The paging state of the query. - * - *

This object represents the next page to be fetched if this query is multi page. It can be - * saved and reused later on the same statement. - * - * @return the paging state or null if there is no next page. - * @see Statement#setPagingState(PagingState) - */ - public PagingState getPagingState() { - if (this.pagingState == null) return null; - return new PagingState( - this.pagingState, this.statement, this.protocolVersion, this.codecRegistry); - } - - /** - * Returns the "raw" paging state of the query. - * - *

Contrary to {@link #getPagingState()}, there will be no validation when this is later - * reinjected into a statement. - * - * @return the paging state or null if there is no next page. - * @see Statement#setPagingStateUnsafe(byte[]) - */ - public byte[] getPagingStateUnsafe() { - if (this.pagingState == null) return null; - return Bytes.getArray(this.pagingState); - } - - /** - * Whether the cluster had reached schema agreement after the execution of this query. - * - *

After a successful schema-altering query (ex: creating a table), the driver will check if - * the cluster's nodes agree on the new schema version. If not, it will keep retrying for a given - * delay (configurable via {@link Cluster.Builder#withMaxSchemaAgreementWaitSeconds(int)}). - * - *

If this method returns {@code false}, clients can call {@link - * Metadata#checkSchemaAgreement()} later to perform the check manually. - * - *

Note that the schema agreement check is only performed for schema-altering queries For other - * query types, this method will always return {@code true}. - * - * @return whether the cluster reached schema agreement, or {@code true} for a non schema-altering - * statement. - */ - public boolean isSchemaInAgreement() { - return schemaInAgreement; - } - - void setSchemaInAgreement(boolean schemaAgreement) { - this.schemaInAgreement = schemaAgreement; - } - - /** - * Returns the server-side warnings for this query. - * - *

This feature is only available with {@link ProtocolVersion#V4} or above; with lower - * versions, the returned list will always be empty. - * - * @return the warnings, or an empty list if there are none. - * @since 2.2 - */ - public List getWarnings() { - return warnings; - } - - /** - * Return the incoming payload, that is, the payload that the server sent back with its response, - * if any, or {@code null}, if the server did not include any custom payload. - * - *

This method returns a read-only view of the original map, but its values remain inherently - * mutable. Callers should take care not to modify the returned map in any way. - * - *

This feature is only available with {@link ProtocolVersion#V4} or above; with lower - * versions, this method will always return {@code null}. - * - * @return the custom payload that the server sent back with its response, if any, or {@code - * null}, if the server did not include any custom payload. - * @since 2.2 - */ - public Map getIncomingPayload() { - return incomingPayload; - } - - /** - * Get the statement that has been executed. - * - * @return the statement executed. - */ - public Statement getStatement() { - return this.statement; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ExtendedAuthProvider.java b/driver-core/src/main/java/com/datastax/driver/core/ExtendedAuthProvider.java deleted file mode 100644 index c65baac64b1..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ExtendedAuthProvider.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.AuthenticationException; -import java.net.InetSocketAddress; - -/** - * An auth provider that represents the host as an {@link EndPoint} instead of a raw {@link - * InetSocketAddress}. - * - *

This interface exists solely for backward compatibility: it wasn't possible to change {@link - * AuthProvider} directly, because it would have broken every 3rd-party implementation. - * - *

All built-in providers now implement this interface, and it is recommended that new - * implementations do too. - * - *

When the driver calls an auth provider, it will check if it implements this interface. If so, - * it will call {@link #newAuthenticator(EndPoint, String)}; otherwise it will convert the endpoint - * into an address with {@link EndPoint#resolve()} and call {@link - * AuthProvider#newAuthenticator(InetSocketAddress, String)}. - */ -public interface ExtendedAuthProvider extends AuthProvider { - - /** - * The {@code Authenticator} to use when connecting to {@code endpoint}. - * - * @param endPoint the Cassandra host to connect to. - * @param authenticator the configured authenticator on the host. - * @return The authentication implementation to use. - */ - Authenticator newAuthenticator(EndPoint endPoint, String authenticator) - throws AuthenticationException; - - /** - * @deprecated the driver will never call this method on {@link ExtendedAuthProvider} instances. - * Implementors should throw {@link AssertionError}. - */ - @Override - @Deprecated - Authenticator newAuthenticator(InetSocketAddress host, String authenticator) - throws AuthenticationException; - - class NoAuthProvider implements ExtendedAuthProvider { - - private static final String DSE_AUTHENTICATOR = - "com.datastax.bdp.cassandra.auth.DseAuthenticator"; - - static final String NO_AUTHENTICATOR_MESSAGE = - "Host %s requires authentication, but no authenticator found in Cluster configuration"; - - @Override - public Authenticator newAuthenticator(EndPoint endPoint, String authenticator) { - if (authenticator.equals(DSE_AUTHENTICATOR)) { - return new TransitionalModePlainTextAuthenticator(); - } - throw new AuthenticationException( - endPoint, String.format(NO_AUTHENTICATOR_MESSAGE, endPoint)); - } - - @Override - public Authenticator newAuthenticator(InetSocketAddress host, String authenticator) - throws AuthenticationException { - throw new AssertionError( - "The driver should never call this method on an object that implements " - + this.getClass().getSimpleName()); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ExtendedRemoteEndpointAwareSslOptions.java b/driver-core/src/main/java/com/datastax/driver/core/ExtendedRemoteEndpointAwareSslOptions.java deleted file mode 100644 index cc7eb34ace6..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ExtendedRemoteEndpointAwareSslOptions.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.ssl.SslHandler; - -public interface ExtendedRemoteEndpointAwareSslOptions extends RemoteEndpointAwareSSLOptions { - - /** - * Creates a new SSL handler for the given Netty channel and the given remote endpoint. - * - *

This gets called each time the driver opens a new connection to a Cassandra host. The newly - * created handler will be added to the channel's pipeline to provide SSL support for the - * connection. - * - *

You don't necessarily need to implement this method directly; see the provided - * implementations: {@link RemoteEndpointAwareJdkSSLOptions} and {@link - * RemoteEndpointAwareNettySSLOptions}. - * - * @param channel the channel. - * @param remoteEndpoint the remote endpoint information. - * @return a newly-created {@link SslHandler}. - */ - SslHandler newSSLHandler(SocketChannel channel, EndPoint remoteEndpoint); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Frame.java b/driver-core/src/main/java/com/datastax/driver/core/Frame.java deleted file mode 100644 index 392e09c7bf0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Frame.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.exceptions.FrameTooLongException; -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.ByteToMessageDecoder; -import io.netty.handler.codec.CorruptedFrameException; -import io.netty.handler.codec.LengthFieldBasedFrameDecoder; -import io.netty.handler.codec.MessageToMessageDecoder; -import io.netty.handler.codec.MessageToMessageEncoder; -import io.netty.handler.codec.TooLongFrameException; -import java.util.EnumSet; -import java.util.List; - -/** - * A frame for the CQL binary protocol. - * - *

Each frame contains a fixed size header (8 bytes for V1 and V2, 9 bytes for V3 and V4) - * followed by a variable size body. The content of the body depends on the header opcode value (the - * body can in particular be empty for some opcode values). - * - *

The protocol distinguishes 2 types of frames: requests and responses. Requests are those - * frames sent by the clients to the server, response are the ones sent by the server. Note however - * that the protocol supports server pushes (events) so responses does not necessarily come right - * after a client request. - * - *

Frames for protocol versions 1+2 are defined as: - * - *

- * - *

- *  0         8        16        24        32
- * +---------+---------+---------+---------+
- * | version |  flags  | stream  | opcode  |
- * +---------+---------+---------+---------+
- * |                length                 |
- * +---------+---------+---------+---------+
- * |                                       |
- * .            ...  body ...              .
- * .                                       .
- * .                                       .
- * +---------------------------------------- *
- * 
- * - *

Frames for protocol versions 3+4 are defined as: - * - *

- * - *

- * 0         8        16        24        32         40
- * +---------+---------+---------+---------+---------+
- * | version |  flags  |      stream       | opcode  |
- * +---------+---------+---------+---------+---------+
- * |                length                 |
- * +---------+---------+---------+---------+
- * |                                       |
- * .            ...  body ...              .
- * .                                       .
- * .                                       .
- * +----------------------------------------
- * 
- * - * @see "https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v1.spec" - * @see "https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v2.spec" - * @see "https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v3.spec" - * @see "https://github.com/apache/cassandra/blob/trunk/doc/native_protocol_v4.spec" - */ -class Frame { - - final Header header; - final ByteBuf body; - - Frame(Header header, ByteBuf body) { - this.header = header; - this.body = body; - } - - private static Frame create(ByteBuf fullFrame) { - Header header = Header.decode(fullFrame); - assert header.bodyLength == fullFrame.readableBytes(); - return new Frame(header, fullFrame); - } - - private static int readStreamId(ByteBuf fullFrame, ProtocolVersion version) { - switch (version) { - case V1: - case V2: - return fullFrame.readByte(); - case V3: - case V4: - case V5: - case V6: - return fullFrame.readShort(); - default: - throw version.unsupported(); - } - } - - static Frame create( - ProtocolVersion version, int opcode, int streamId, EnumSet flags, ByteBuf body) { - Header header = new Header(version, flags, streamId, opcode, body.readableBytes()); - return new Frame(header, body); - } - - static class Header { - - final ProtocolVersion version; - final EnumSet flags; - final int streamId; - final int opcode; - final int bodyLength; - - private Header(ProtocolVersion version, int flags, int streamId, int opcode, int bodyLength) { - this(version, Flag.deserialize(flags), streamId, opcode, bodyLength); - } - - Header(ProtocolVersion version, EnumSet flags, int streamId, int opcode, int bodyLength) { - this.version = version; - this.flags = flags; - this.streamId = streamId; - this.opcode = opcode; - this.bodyLength = bodyLength; - } - - Header withNewBodyLength(int newBodyLength) { - return new Header(version, flags, streamId, opcode, newBodyLength); - } - - /** - * Return the expected frame header length in bytes according to the protocol version in use. - * - * @param version the protocol version in use - * @return the expected frame header length in bytes - */ - static int lengthFor(ProtocolVersion version) { - switch (version) { - case V1: - case V2: - return 8; - case V3: - case V4: - case V5: - case V6: - return 9; - default: - throw version.unsupported(); - } - } - - public void encodeInto(ByteBuf destination) { - // Don't bother with the direction, we only send requests. - destination.writeByte(version.toInt()); - destination.writeByte(Flag.serialize(flags)); - switch (version) { - case V1: - case V2: - destination.writeByte(streamId); - break; - case V3: - case V4: - case V5: - case V6: - destination.writeShort(streamId); - break; - default: - throw version.unsupported(); - } - destination.writeByte(opcode); - destination.writeInt(bodyLength); - } - - static Header decode(ByteBuf buffer) { - assert buffer.readableBytes() >= 1 - : String.format("Frame too short (%d bytes)", buffer.readableBytes()); - - int versionBytes = buffer.readByte(); - // version first byte is the "direction" of the frame (request or response) - ProtocolVersion version = ProtocolVersion.fromInt(versionBytes & 0x7F); - int hdrLen = Header.lengthFor(version); - assert buffer.readableBytes() >= (hdrLen - 1) - : String.format("Frame too short (%d bytes)", buffer.readableBytes()); - - int flags = buffer.readByte(); - int streamId = readStreamId(buffer, version); - int opcode = buffer.readByte(); - int length = buffer.readInt(); - - return new Header(version, flags, streamId, opcode, length); - } - - enum Flag { - // The order of that enum matters!! - COMPRESSED, - TRACING, - CUSTOM_PAYLOAD, - WARNING, - USE_BETA; - - static EnumSet deserialize(int flags) { - EnumSet set = EnumSet.noneOf(Flag.class); - Flag[] values = Flag.values(); - for (int n = 0; n < 8; n++) { - if ((flags & (1 << n)) != 0) set.add(values[n]); - } - return set; - } - - static int serialize(EnumSet flags) { - int i = 0; - for (Flag flag : flags) i |= 1 << flag.ordinal(); - return i; - } - } - } - - Frame with(ByteBuf newBody) { - return new Frame(header.withNewBodyLength(newBody.readableBytes()), newBody); - } - - static final class Decoder extends ByteToMessageDecoder { - private DecoderForStreamIdSize decoder; - - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List out) - throws Exception { - if (buffer.readableBytes() < 1) return; - - // Initialize sub decoder on first message. No synchronization needed as - // decode is always called from same thread. - if (decoder == null) { - int version = buffer.getByte(buffer.readerIndex()); - // version first bit is the "direction" of the frame (request or response) - version = version & 0x7F; - decoder = new DecoderForStreamIdSize(version, version >= 3 ? 2 : 1); - } - - Object frame = decoder.decode(ctx, buffer); - if (frame != null) out.add(frame); - } - - static class DecoderForStreamIdSize extends LengthFieldBasedFrameDecoder { - // The maximum response frame length allowed. Note that C* does not currently restrict the - // length of its responses (CASSANDRA-12630). - private static final int MAX_FRAME_LENGTH = - SystemProperties.getInt("com.datastax.driver.NATIVE_TRANSPORT_MAX_FRAME_SIZE_IN_MB", 256) - * 1024 - * 1024; // 256 MB - private final int protocolVersion; - - DecoderForStreamIdSize(int protocolVersion, int streamIdSize) { - super(MAX_FRAME_LENGTH, /*lengthOffset=*/ 3 + streamIdSize, 4, 0, 0, true); - this.protocolVersion = protocolVersion; - } - - @Override - protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { - // Capture current index in case we need to get the stream id. - // If a TooLongFrameException is thrown the readerIndex will advance to the end of - // the buffer (or past the frame) so we need the position as we entered this method. - int curIndex = buffer.readerIndex(); - try { - ByteBuf frame = (ByteBuf) super.decode(ctx, buffer); - if (frame == null) { - return null; - } - // Do not deallocate `frame` just yet, because it is stored as Frame.body and will be used - // in Message.ProtocolDecoder or Frame.Decompressor if compression is enabled (we - // deallocate - // it there). - Frame theFrame = Frame.create(frame); - // Validate the opcode (this will throw if it's not a response) - Message.Response.Type.fromOpcode(theFrame.header.opcode); - return theFrame; - } catch (CorruptedFrameException e) { - throw new DriverInternalError(e); - } catch (TooLongFrameException e) { - int streamId = - protocolVersion > 2 ? buffer.getShort(curIndex + 2) : buffer.getByte(curIndex + 2); - throw new FrameTooLongException(streamId); - } - } - } - } - - @ChannelHandler.Sharable - static class Encoder extends MessageToMessageEncoder { - - @Override - protected void encode(ChannelHandlerContext ctx, Frame frame, List out) - throws Exception { - ProtocolVersion protocolVersion = frame.header.version; - ByteBuf header = ctx.alloc().ioBuffer(Frame.Header.lengthFor(protocolVersion)); - frame.header.encodeInto(header); - - out.add(header); - out.add(frame.body); - } - } - - static class Decompressor extends MessageToMessageDecoder { - - private final FrameCompressor compressor; - - Decompressor(FrameCompressor compressor) { - assert compressor != null; - this.compressor = compressor; - } - - @Override - protected void decode(ChannelHandlerContext ctx, Frame frame, List out) - throws Exception { - if (frame.header.flags.contains(Header.Flag.COMPRESSED)) { - // All decompressors allocate a new buffer for the decompressed data, so this is the last - // time - // we have a reference to the compressed body (and therefore a chance to release it). - ByteBuf compressedBody = frame.body; - try { - out.add(compressor.decompress(frame)); - } finally { - compressedBody.release(); - } - } else { - out.add(frame); - } - } - } - - static class Compressor extends MessageToMessageEncoder { - - private final FrameCompressor compressor; - - Compressor(FrameCompressor compressor) { - assert compressor != null; - this.compressor = compressor; - } - - @Override - protected void encode(ChannelHandlerContext ctx, Frame frame, List out) - throws Exception { - // Never compress STARTUP messages - if (frame.header.opcode == Message.Request.Type.STARTUP.opcode) { - out.add(frame); - } else { - frame.header.flags.add(Header.Flag.COMPRESSED); - // See comment in decode() - ByteBuf uncompressedBody = frame.body; - try { - out.add(compressor.compress(frame)); - } finally { - uncompressedBody.release(); - } - } - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/FrameCompressor.java b/driver-core/src/main/java/com/datastax/driver/core/FrameCompressor.java deleted file mode 100644 index a7d6daf21e6..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/FrameCompressor.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.buffer.ByteBuf; -import java.io.IOException; -import java.nio.ByteBuffer; - -abstract class FrameCompressor { - - abstract Frame compress(Frame frame) throws IOException; - - /** - * Unlike {@link #compress(Frame)}, this variant does not store the uncompressed length if the - * underlying algorithm does not do it natively (like LZ4). It must be stored separately and - * passed back to {@link #decompress(ByteBuf, int)}. - */ - abstract ByteBuf compress(ByteBuf buffer) throws IOException; - - abstract Frame decompress(Frame frame) throws IOException; - - abstract ByteBuf decompress(ByteBuf buffer, int uncompressedLength) throws IOException; - - protected static ByteBuffer inputNioBuffer(ByteBuf buf) { - // Using internalNioBuffer(...) as we only hold the reference in this method and so can - // reduce Object allocations. - int index = buf.readerIndex(); - int len = buf.readableBytes(); - return buf.nioBufferCount() == 1 - ? buf.internalNioBuffer(index, len) - : buf.nioBuffer(index, len); - } - - protected static ByteBuffer outputNioBuffer(ByteBuf buf) { - int index = buf.writerIndex(); - int len = buf.writableBytes(); - return buf.nioBufferCount() == 1 - ? buf.internalNioBuffer(index, len) - : buf.nioBuffer(index, len); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/FramingFormatHandler.java b/driver-core/src/main/java/com/datastax/driver/core/FramingFormatHandler.java deleted file mode 100644 index 91459a1ab34..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/FramingFormatHandler.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.Message.Response.Type; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.codec.MessageToMessageDecoder; -import java.util.List; - -/** - * A handler to deal with different protocol framing formats. - * - *

This handler detects when a handshake is successful; then, if necessary, adapts the pipeline - * to the modern framing format introduced in protocol v5. - */ -public class FramingFormatHandler extends MessageToMessageDecoder { - - private final Connection.Factory factory; - - FramingFormatHandler(Connection.Factory factory) { - this.factory = factory; - } - - @Override - protected void decode(ChannelHandlerContext ctx, Frame frame, List out) throws Exception { - boolean handshakeSuccessful = - frame.header.opcode == Type.READY.opcode || frame.header.opcode == Type.AUTHENTICATE.opcode; - if (handshakeSuccessful) { - // By default, the pipeline is configured for legacy framing since this is the format used - // by all protocol versions until handshake; after handshake however, we need to switch to - // modern framing for protocol v5 and higher. - if (frame.header.version.compareTo(ProtocolVersion.V5) >= 0) { - switchToModernFraming(ctx); - } - // once the handshake is successful, the framing format cannot change anymore; - // we can safely remove ourselves from the pipeline. - ctx.pipeline().remove("framingFormatHandler"); - } - out.add(frame); - } - - private void switchToModernFraming(ChannelHandlerContext ctx) { - ChannelPipeline pipeline = ctx.pipeline(); - SegmentCodec segmentCodec = - new SegmentCodec( - ctx.channel().alloc(), factory.configuration.getProtocolOptions().getCompression()); - - // Outbound: "message -> segment -> bytes" instead of "message -> frame -> bytes" - Message.ProtocolEncoder requestEncoder = - (Message.ProtocolEncoder) pipeline.get("messageEncoder"); - pipeline.replace( - "messageEncoder", - "messageToSegmentEncoder", - new MessageToSegmentEncoder(ctx.channel().alloc(), requestEncoder)); - pipeline.replace( - "frameEncoder", "segmentToBytesEncoder", new SegmentToBytesEncoder(segmentCodec)); - - // Inbound: "frame <- segment <- bytes" instead of "frame <- bytes" - pipeline.replace( - "frameDecoder", "bytesToSegmentDecoder", new BytesToSegmentDecoder(segmentCodec)); - pipeline.addAfter( - "bytesToSegmentDecoder", "segmentToFrameDecoder", new SegmentToFrameDecoder()); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/FunctionMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/FunctionMetadata.java deleted file mode 100644 index 55ed77053dd..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/FunctionMetadata.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.collect.ImmutableMap; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Describes a CQL function (created with {@code CREATE FUNCTION...}). */ -public class FunctionMetadata { - private static final Logger logger = LoggerFactory.getLogger(FunctionMetadata.class); - - private final KeyspaceMetadata keyspace; - private final String simpleName; - private final Map arguments; - private final String body; - private final boolean calledOnNullInput; - private final String language; - private final DataType returnType; - - private FunctionMetadata( - KeyspaceMetadata keyspace, - String simpleName, - Map arguments, - String body, - boolean calledOnNullInput, - String language, - DataType returnType) { - this.keyspace = keyspace; - this.simpleName = simpleName; - this.arguments = arguments; - this.body = body; - this.calledOnNullInput = calledOnNullInput; - this.language = language; - this.returnType = returnType; - } - - // Cassandra < 3.0: - // CREATE TABLE system.schema_functions ( - // keyspace_name text, - // function_name text, - // signature frozen>, - // argument_names list, - // argument_types list, - // body text, - // called_on_null_input boolean, - // language text, - // return_type text, - // PRIMARY KEY (keyspace_name, function_name, signature) - // ) WITH CLUSTERING ORDER BY (function_name ASC, signature ASC) - // - // Cassandra >= 3.0: - // CREATE TABLE system_schema.functions ( - // keyspace_name text, - // function_name text, - // argument_names frozen>, - // argument_types frozen>, - // body text, - // called_on_null_input boolean, - // language text, - // return_type text, - // PRIMARY KEY (keyspace_name, function_name, argument_types) - // ) WITH CLUSTERING ORDER BY (function_name ASC, argument_types ASC) - // - - static FunctionMetadata build( - KeyspaceMetadata ksm, Row row, VersionNumber version, Cluster cluster) { - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - String simpleName = row.getString("function_name"); - List argumentNames = row.getList("argument_names", String.class); - // this will be a list of C* types in 2.2 and a list of CQL types in 3.0 - List argumentTypes = row.getList("argument_types", String.class); - Map arguments = - buildArguments(ksm, argumentNames, argumentTypes, version, cluster); - if (argumentNames.size() != argumentTypes.size()) { - String fullName = Metadata.fullFunctionName(simpleName, arguments.values()); - logger.error( - String.format( - "Error parsing definition of function %1$s.%2$s: the number of argument names and types don't match." - + "Cluster.getMetadata().getKeyspace(\"%1$s\").getFunction(\"%2$s\") will be missing.", - ksm.getName(), fullName)); - return null; - } - String body = row.getString("body"); - boolean calledOnNullInput = row.getBool("called_on_null_input"); - String language = row.getString("language"); - DataType returnType; - if (version.getMajor() >= 3.0) { - returnType = - DataTypeCqlNameParser.parse( - row.getString("return_type"), - cluster, - ksm.getName(), - ksm.userTypes, - null, - false, - false); - } else { - returnType = - DataTypeClassNameParser.parseOne( - row.getString("return_type"), protocolVersion, codecRegistry); - } - return new FunctionMetadata( - ksm, simpleName, arguments, body, calledOnNullInput, language, returnType); - } - - // Note: the caller ensures that names and types have the same size - private static Map buildArguments( - KeyspaceMetadata ksm, - List names, - List types, - VersionNumber version, - Cluster cluster) { - if (names.isEmpty()) return Collections.emptyMap(); - ImmutableMap.Builder builder = ImmutableMap.builder(); - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - Iterator iterTypes = types.iterator(); - for (String name : names) { - DataType type; - if (version.getMajor() >= 3) { - type = - DataTypeCqlNameParser.parse( - iterTypes.next(), cluster, ksm.getName(), ksm.userTypes, null, false, false); - } else { - type = DataTypeClassNameParser.parseOne(iterTypes.next(), protocolVersion, codecRegistry); - } - builder.put(name, type); - } - return builder.build(); - } - - /** - * Returns a CQL query representing this function in human readable form. - * - *

This method is equivalent to {@link #asCQLQuery} but the output is formatted. - * - * @return the CQL query representing this function. - */ - public String exportAsString() { - return asCQLQuery(true); - } - - /** - * Returns a CQL query representing this function. - * - *

This method returns a single 'CREATE FUNCTION' query corresponding to this function - * definition. - * - * @return the 'CREATE FUNCTION' query corresponding to this function. - */ - public String asCQLQuery() { - return asCQLQuery(false); - } - - @Override - public String toString() { - return asCQLQuery(false); - } - - private String asCQLQuery(boolean formatted) { - - StringBuilder sb = new StringBuilder("CREATE FUNCTION "); - - sb.append(Metadata.quoteIfNecessary(keyspace.getName())) - .append('.') - .append(Metadata.quoteIfNecessary(simpleName)) - .append('('); - - boolean first = true; - for (Map.Entry entry : arguments.entrySet()) { - if (first) first = false; - else sb.append(','); - String name = entry.getKey(); - DataType type = entry.getValue(); - sb.append(Metadata.quoteIfNecessary(name)) - .append(' ') - .append(type.asFunctionParameterString()); - } - sb.append(')'); - - TableMetadata.spaceOrNewLine(sb, formatted) - .append(calledOnNullInput ? "CALLED ON NULL INPUT" : "RETURNS NULL ON NULL INPUT"); - - TableMetadata.spaceOrNewLine(sb, formatted) - .append("RETURNS ") - .append(returnType.asFunctionParameterString()); - - TableMetadata.spaceOrNewLine(sb, formatted).append("LANGUAGE ").append(language); - - TableMetadata.spaceOrNewLine(sb, formatted).append("AS '").append(body).append("';"); - - return sb.toString(); - } - - /** - * Returns the keyspace this function belongs to. - * - * @return the keyspace metadata of the keyspace this function belongs to. - */ - public KeyspaceMetadata getKeyspace() { - return keyspace; - } - - /** - * Returns the CQL signature of this function. - * - *

This is the name of the function, followed by the names of the argument types between - * parentheses, for example {@code sum(int,int)}. - * - *

Note that the returned signature is not qualified with the keyspace name. - * - * @return the signature of this function. - */ - public String getSignature() { - StringBuilder sb = new StringBuilder(); - sb.append(Metadata.quoteIfNecessary(simpleName)).append('('); - boolean first = true; - for (DataType type : arguments.values()) { - if (first) first = false; - else sb.append(','); - sb.append(type.asFunctionParameterString()); - } - sb.append(')'); - return sb.toString(); - } - - /** - * Returns the simple name of this function. - * - *

This is the name of the function, without arguments. Note that functions can be overloaded - * with different argument lists, therefore the simple name may not be unique. For example, {@code - * sum(int,int)} and {@code sum(int,int,int)} both have the simple name {@code sum}. - * - * @return the simple name of this function. - * @see #getSignature() - */ - public String getSimpleName() { - return simpleName; - } - - /** - * Returns the names and types of this function's arguments. - * - * @return a map from argument name to argument type. - */ - public Map getArguments() { - return arguments; - } - - /** - * Returns the body of this function. - * - * @return the body. - */ - public String getBody() { - return body; - } - - /** - * Indicates whether this function's body gets called on null input. - * - *

This is {@code true} if the function was created with {@code CALLED ON NULL INPUT}, and - * {@code false} if it was created with {@code RETURNS NULL ON NULL INPUT}. - * - * @return whether this function's body gets called on null input. - */ - public boolean isCalledOnNullInput() { - return calledOnNullInput; - } - - /** - * Returns the programming language in which this function's body is written. - * - * @return the language. - */ - public String getLanguage() { - return language; - } - - /** - * Returns the return type of this function. - * - * @return the return type. - */ - public DataType getReturnType() { - return returnType; - } - - @Override - public boolean equals(Object other) { - if (other == this) return true; - - if (other instanceof FunctionMetadata) { - FunctionMetadata that = (FunctionMetadata) other; - return this.keyspace.getName().equals(that.keyspace.getName()) - && this.arguments.equals(that.arguments) - && this.body.equals(that.body) - && this.calledOnNullInput == that.calledOnNullInput - && this.language.equals(that.language) - && this.returnType.equals(that.returnType); - } - return false; - } - - @Override - public int hashCode() { - return MoreObjects.hashCode( - keyspace.getName(), arguments, body, calledOnNullInput, language, returnType); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/GettableByIndexData.java b/driver-core/src/main/java/com/datastax/driver/core/GettableByIndexData.java deleted file mode 100644 index c6fcf4c81bc..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/GettableByIndexData.java +++ /dev/null @@ -1,589 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.CodecNotFoundException; -import com.datastax.driver.core.exceptions.InvalidTypeException; -import com.google.common.reflect.TypeToken; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -/** Collection of (typed) CQL values that can be retrieved by index (starting at zero). */ -public interface GettableByIndexData { - - /** - * Returns whether the {@code i}th value is NULL. - * - * @param i the index ({@code 0 <= i < size()}) of the value to check. - * @return whether the {@code i}th value is NULL. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - */ - public boolean isNull(int i); - - /** - * Returns the {@code i}th value as a boolean. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code boolean} (for CQL type {@code boolean}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the boolean value of the {@code i}th element. If the value is NULL, {@code false} is - * returned. If you need to distinguish NULL and false values, check first with {@link - * #isNull(int)} or use {@code get(i, Boolean.class)}. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a boolean. - */ - public boolean getBool(int i); - - /** - * Returns the {@code i}th value as a byte. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code byte} (for CQL type {@code tinyint}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a byte. If the value is NULL, {@code 0} is - * returned. If you need to distinguish NULL and 0, check first with {@link #isNull(int)} or - * use {@code get(i, Byte.class)}. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a byte. - */ - public byte getByte(int i); - - /** - * Returns the {@code i}th value as a short. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code short} (for CQL type {@code smallint}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a short. If the value is NULL, {@code 0} is - * returned. If you need to distinguish NULL and 0, check first with {@link #isNull(int)} or - * use {@code get(i, Short.class)}. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a short. - */ - public short getShort(int i); - - /** - * Returns the {@code i}th value as an integer. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code int} (for CQL type {@code int}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as an integer. If the value is NULL, {@code 0} is - * returned. If you need to distinguish NULL and 0, check first with {@link #isNull(int)} or - * use {@code get(i, Integer.class)}. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to an int. - */ - public int getInt(int i); - - /** - * Returns the {@code i}th value as a long. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code byte} (for CQL types {@code bigint} and {@code counter}, this will be the - * built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a long. If the value is NULL, {@code 0L} is - * returned. If you need to distinguish NULL and 0L, check first with {@link #isNull(int)} or - * use {@code get(i, Long.class)}. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a long. - */ - public long getLong(int i); - - /** - * Returns the {@code i}th value as a date. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code Date} (for CQL type {@code timestamp}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a data. If the value is NULL, {@code null} is - * returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a {@code Date}. - */ - public Date getTimestamp(int i); - - /** - * Returns the {@code i}th value as a date (without time). - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@link LocalDate} (for CQL type {@code date}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as an date. If the value is NULL, {@code null} is - * returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a {@code LocalDate}. - */ - public LocalDate getDate(int i); - - /** - * Returns the {@code i}th value as a long in nanoseconds since midnight. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code long} (for CQL type {@code time}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a long. If the value is NULL, {@code 0L} is - * returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a long. - */ - public long getTime(int i); - - /** - * Returns the {@code i}th value as a float. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code float} (for CQL type {@code float}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a float. If the value is NULL, {@code 0.0f} is - * returned. If you need to distinguish NULL and 0.0f, check first with {@link #isNull(int)} - * or use {@code get(i, Float.class)}. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a float. - */ - public float getFloat(int i); - - /** - * Returns the {@code i}th value as a double. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code double} (for CQL type {@code double}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a double. If the value is NULL, {@code 0.0} is - * returned. If you need to distinguish NULL and 0.0, check first with {@link #isNull(int)} or - * use {@code get(i, Double.class)}. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a double. - */ - public double getDouble(int i); - - /** - * Returns the {@code i}th value as a {@code ByteBuffer}. - * - *

This method does not use any codec; it returns a copy of the binary representation of the - * value. It is up to the caller to convert the returned value appropriately. - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a ByteBuffer. If the value is NULL, {@code - * null} is returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - */ - public ByteBuffer getBytesUnsafe(int i); - - /** - * Returns the {@code i}th value as a byte array. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code ByteBuffer} (for CQL type {@code blob}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a byte array. If the value is NULL, {@code - * null} is returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a {@code ByteBuffer}. - */ - public ByteBuffer getBytes(int i); - - /** - * Returns the {@code i}th value as a string. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java string (for CQL types {@code text}, {@code varchar} and {@code ascii}, this will - * be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a string. If the value is NULL, {@code null} is - * returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a string. - */ - public String getString(int i); - - /** - * Returns the {@code i}th value as a variable length integer. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code BigInteger} (for CQL type {@code varint}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a variable length integer. If the value is - * NULL, {@code null} is returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a {@code BigInteger}. - */ - public BigInteger getVarint(int i); - - /** - * Returns the {@code i}th value as a variable length decimal. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code BigDecimal} (for CQL type {@code decimal}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a variable length decimal. If the value is - * NULL, {@code null} is returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a {@code BigDecimal}. - */ - public BigDecimal getDecimal(int i); - - /** - * Returns the {@code i}th value as a UUID. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code UUID} (for CQL types {@code uuid} and {@code timeuuid}, this will be the - * built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a UUID. If the value is NULL, {@code null} is - * returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a {@code UUID}. - */ - public UUID getUUID(int i); - - /** - * Returns the {@code i}th value as an InetAddress. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to an {@code InetAddress} (for CQL type {@code inet}, this will be the built-in codec). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as an InetAddress. If the value is NULL, {@code - * null} is returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a {@code InetAddress}. - */ - public InetAddress getInet(int i); - - /** - * Returns the {@code i}th value as a list. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a list of the specified type. - * - *

If the type of the elements is generic, use {@link #getList(int, TypeToken)}. - * - *

Implementation note: the actual {@link List} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will be mapped to an empty collection (note that Cassandra - * makes no distinction between {@code NULL} and an empty collection). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @param elementsClass the class for the elements of the list to retrieve. - * @return the value of the {@code i}th element as a list of {@code T} objects. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a list. - */ - public List getList(int i, Class elementsClass); - - /** - * Returns the {@code i}th value as a list. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a list of the specified type. - * - *

Use this variant with nested collections, which produce a generic element type: - * - *

-   * {@code List> l = row.getList(1, new TypeToken>() {});}
-   * 
- * - *

Implementation note: the actual {@link List} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @param elementsType the type of the elements of the list to retrieve. - * @return the value of the {@code i}th element as a list of {@code T} objects. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a list. - */ - public List getList(int i, TypeToken elementsType); - - /** - * Returns the {@code i}th value as a set. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a set of the specified type. - * - *

If the type of the elements is generic, use {@link #getSet(int, TypeToken)}. - * - *

Implementation note: the actual {@link Set} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @param elementsClass the class for the elements of the set to retrieve. - * @return the value of the {@code i}th element as a set of {@code T} objects. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a set. - */ - public Set getSet(int i, Class elementsClass); - - /** - * Returns the {@code i}th value as a set. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a set of the specified type. - * - *

Use this variant with nested collections, which produce a generic element type: - * - *

-   * {@code Set> l = row.getSet(1, new TypeToken>() {});}
-   * 
- * - *

Implementation note: the actual {@link Set} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @param elementsType the type for the elements of the set to retrieve. - * @return the value of the {@code i}th element as a set of {@code T} objects. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a set. - */ - public Set getSet(int i, TypeToken elementsType); - - /** - * Returns the {@code i}th value as a map. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a map of the specified types. - * - *

If the type of the keys and/or values is generic, use {@link #getMap(int, TypeToken, - * TypeToken)}. - * - *

Implementation note: the actual {@link Map} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @param keysClass the class for the keys of the map to retrieve. - * @param valuesClass the class for the values of the map to retrieve. - * @return the value of the {@code i}th element as a map of {@code K} to {@code V} objects. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a map. - */ - public Map getMap(int i, Class keysClass, Class valuesClass); - - /** - * Returns the {@code i}th value as a map. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a map of the specified types. - * - *

Use this variant with nested collections, which produce a generic element type: - * - *

-   * {@code Map> l = row.getMap(1, TypeToken.of(Integer.class), new TypeToken>() {});}
-   * 
- * - *

Implementation note: the actual {@link Map} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @param keysType the type for the keys of the map to retrieve. - * @param valuesType the type for the values of the map to retrieve. - * @return the value of the {@code i}th element as a map of {@code K} to {@code V} objects. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a map. - */ - public Map getMap(int i, TypeToken keysType, TypeToken valuesType); - - /** - * Return the {@code i}th value as a UDT value. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code UDTValue} (if the CQL type is a UDT, the registry will generate a codec - * automatically). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a UDT value. If the value is NULL, then {@code - * null} will be returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a {@code UDTValue}. - */ - public UDTValue getUDTValue(int i); - - /** - * Return the {@code i}th value as a tuple value. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code TupleValue} (if the CQL type is a tuple, the registry will generate a codec - * automatically). - * - * @param i the index ({@code 0 <= i < size()}) to retrieve. - * @return the value of the {@code i}th element as a tuple value. If the value is NULL, then - * {@code null} will be returned. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to a {@code TupleValue}. - */ - public TupleValue getTupleValue(int i); - - /** - * Returns the {@code i}th value as the Java type matching its CQL type. - * - *

This method uses the {@link CodecRegistry} to find the first codec that handles the - * underlying CQL type. The Java type of the returned object will be determined by the codec that - * was selected. - * - *

Use this method to dynamically inspect elements when types aren't known in advance, for - * instance if you're writing a generic row logger. If you know the target Java type, it is - * generally preferable to use typed getters, such as the ones for built-in types ({@link - * #getBool(int)}, {@link #getInt(int)}, etc.), or {@link #get(int, Class)} and {@link #get(int, - * TypeToken)} for custom types. - * - * @param i the index to retrieve. - * @return the value of the {@code i}th value as the Java type matching its CQL type. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @see CodecRegistry#codecFor(DataType) - */ - public Object getObject(int i); - - /** - * Returns the {@code i}th value converted to the given Java type. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to the given Java type. - * - *

If the target type is generic, use {@link #get(int, TypeToken)}. - * - *

Implementation note: the actual object returned by this method will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent; by default, a CQL {@code NULL} value translates to - * {@code null} for simple CQL types, UDTs and tuples, and to empty collections for all CQL - * collection types. - * - * @param i the index to retrieve. - * @param targetClass The Java type the value should be converted to. - * @return the value of the {@code i}th value converted to the given Java type. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to {@code targetClass}. - */ - T get(int i, Class targetClass); - - /** - * Returns the {@code i}th value converted to the given Java type. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to the given Java type. - * - *

Implementation note: the actual object returned by this method will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent; by default, a CQL {@code NULL} value translates to - * {@code null} for simple CQL types, UDTs and tuples, and to empty collections for all CQL - * collection types. - * - * @param i the index to retrieve. - * @param targetType The Java type the value should be converted to. - * @return the value of the {@code i}th value converted to the given Java type. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the element's CQL - * type to {@code targetType}. - */ - T get(int i, TypeToken targetType); - - /** - * Returns the {@code i}th value converted using the given {@link TypeCodec}. - * - *

This method entirely bypasses the {@link CodecRegistry} and forces the driver to use the - * given codec instead. This can be useful if the codec would collide with a previously registered - * one, or if you want to use the codec just once without registering it. - * - *

It is the caller's responsibility to ensure that the given codec {@link - * TypeCodec#accepts(DataType) accepts} the underlying CQL type; failing to do so may result in - * {@link InvalidTypeException}s being thrown. - * - *

Implementation note: the actual object returned by this method will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent; by default, a CQL {@code NULL} value translates to - * {@code null} for simple CQL types, UDTs and tuples, and to empty collections for all CQL - * collection types. - * - * @param i the index to retrieve. - * @param codec The {@link TypeCodec} to use to deserialize the value; may not be {@code null}. - * @return the value of the {@code i}th value converted using the given {@link TypeCodec}. - * @throws InvalidTypeException if the given codec does not {@link TypeCodec#accepts(DataType) - * accept} the underlying CQL type. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - */ - T get(int i, TypeCodec codec); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/GettableByNameData.java b/driver-core/src/main/java/com/datastax/driver/core/GettableByNameData.java deleted file mode 100644 index 7bc7b5bbec4..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/GettableByNameData.java +++ /dev/null @@ -1,590 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.CodecNotFoundException; -import com.datastax.driver.core.exceptions.InvalidTypeException; -import com.google.common.reflect.TypeToken; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -/** Collection of (typed) CQL values that can be retrieved by name. */ -public interface GettableByNameData { - - /** - * Returns whether the value for {@code name} is NULL. - * - * @param name the name to check. - * @return whether the value for {@code name} is NULL. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - */ - public boolean isNull(String name); - - /** - * Returns the value for {@code name} as a boolean. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code boolean} (for CQL type {@code boolean}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the boolean value for {@code name}. If the value is NULL, {@code false} is returned. If - * you need to distinguish NULL and false values, check first with {@link #isNull(String)} or - * use {@code get(name, Boolean.class)}. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a boolean. - */ - public boolean getBool(String name); - - /** - * Returns the value for {@code name} as a byte. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code byte} (for CQL type {@code tinyint}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a byte. If the value is NULL, {@code 0} is returned. If - * you need to distinguish NULL and 0, check first with {@link #isNull(String)} or use {@code - * get(name, Byte.class)}. {@code 0} is returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a byte. - */ - public byte getByte(String name); - - /** - * Returns the value for {@code name} as a short. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code short} (for CQL type {@code smallint}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a short. If the value is NULL, {@code 0} is returned. If - * you need to distinguish NULL and 0, check first with {@link #isNull(String)} or use {@code - * get(name, Short.class)}. {@code 0} is returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a short. - */ - public short getShort(String name); - - /** - * Returns the value for {@code name} as an integer. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code int} (for CQL type {@code int}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as an integer. If the value is NULL, {@code 0} is returned. - * If you need to distinguish NULL and 0, check first with {@link #isNull(String)} or use - * {@code get(name, Integer.class)}. {@code 0} is returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to an int. - */ - public int getInt(String name); - - /** - * Returns the value for {@code name} as a long. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code byte} (for CQL types {@code bigint} and {@code counter}, this will be the - * built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a long. If the value is NULL, {@code 0L} is returned. If - * you need to distinguish NULL and 0L, check first with {@link #isNull(String)} or use {@code - * get(name, Long.class)}. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a long. - */ - public long getLong(String name); - - /** - * Returns the value for {@code name} as a date. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code Date} (for CQL type {@code timestamp}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a date. If the value is NULL, {@code null} is returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a {@code Date}. - */ - public Date getTimestamp(String name); - - /** - * Returns the value for {@code name} as a date (without time). - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@link LocalDate} (for CQL type {@code date}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a date. If the value is NULL, {@code null} is returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a {@code LocalDate}. - */ - public LocalDate getDate(String name); - - /** - * Returns the value for {@code name} as a long in nanoseconds since midnight. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code long} (for CQL type {@code time}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a long. If the value is NULL, {@code 0L} is returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a long. - */ - public long getTime(String name); - - /** - * Returns the value for {@code name} as a float. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code float} (for CQL type {@code float}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a float. If the value is NULL, {@code 0.0f} is returned. - * If you need to distinguish NULL and 0.0f, check first with {@link #isNull(String)} or use - * {@code get(name, Float.class)}. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a float. - */ - public float getFloat(String name); - - /** - * Returns the value for {@code name} as a double. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code double} (for CQL type {@code double}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a double. If the value is NULL, {@code 0.0} is returned. - * If you need to distinguish NULL and 0.0, check first with {@link #isNull(String)} or use - * {@code get(name, Double.class)}. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a double. - */ - public double getDouble(String name); - - /** - * Returns the value for {@code name} as a ByteBuffer. - * - *

This method does not use any codec; it returns a copy of the binary representation of the - * value. It is up to the caller to convert the returned value appropriately. - * - *

Note: this method always return the bytes composing the value, even if the column is not of - * type BLOB. That is, this method never throw an InvalidTypeException. However, if the type is - * not BLOB, it is up to the caller to handle the returned value correctly. - * - * @param name the name to retrieve. - * @return the value for {@code name} as a ByteBuffer. If the value is NULL, {@code null} is - * returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - */ - public ByteBuffer getBytesUnsafe(String name); - - /** - * Returns the value for {@code name} as a byte array. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java {@code ByteBuffer} (for CQL type {@code blob}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a byte array. If the value is NULL, {@code null} is - * returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a {@code ByteBuffer}. - */ - public ByteBuffer getBytes(String name); - - /** - * Returns the value for {@code name} as a string. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a Java string (for CQL types {@code text}, {@code varchar} and {@code ascii}, this will - * be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a string. If the value is NULL, {@code null} is returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a string. - */ - public String getString(String name); - - /** - * Returns the value for {@code name} as a variable length integer. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code BigInteger} (for CQL type {@code varint}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a variable length integer. If the value is NULL, {@code - * null} is returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a {@code BigInteger}. - */ - public BigInteger getVarint(String name); - - /** - * Returns the value for {@code name} as a variable length decimal. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code BigDecimal} (for CQL type {@code decimal}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a variable length decimal. If the value is NULL, {@code - * null} is returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a {@code BigDecimal}. - */ - public BigDecimal getDecimal(String name); - - /** - * Returns the value for {@code name} as a UUID. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code UUID} (for CQL types {@code uuid} and {@code timeuuid}, this will be the - * built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as a UUID. If the value is NULL, {@code null} is returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a {@code UUID}. - */ - public UUID getUUID(String name); - - /** - * Returns the value for {@code name} as an InetAddress. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to an {@code InetAddress} (for CQL type {@code inet}, this will be the built-in codec). - * - * @param name the name to retrieve. - * @return the value for {@code name} as an InetAddress. If the value is NULL, {@code null} is - * returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a {@code InetAddress}. - */ - public InetAddress getInet(String name); - - /** - * Returns the value for {@code name} as a list. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a list of the specified type. - * - *

If the type of the elements is generic, use {@link #getList(String, TypeToken)}. - * - *

Implementation note: the actual {@link List} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param name the name to retrieve. - * @param elementsClass the class for the elements of the list to retrieve. - * @return the value of the {@code i}th element as a list of {@code T} objects. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a list. - */ - public List getList(String name, Class elementsClass); - - /** - * Returns the value for {@code name} as a list. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a list of the specified type. - * - *

Use this variant with nested collections, which produce a generic element type: - * - *

-   * {@code List> l = row.getList("theColumn", new TypeToken>() {});}
-   * 
- * - *

Implementation note: the actual {@link List} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param name the name to retrieve. - * @param elementsType the type for the elements of the list to retrieve. - * @return the value of the {@code i}th element as a list of {@code T} objects. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a list. - */ - public List getList(String name, TypeToken elementsType); - - /** - * Returns the value for {@code name} as a set. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a set of the specified type. - * - *

If the type of the elements is generic, use {@link #getSet(String, TypeToken)}. - * - *

Implementation note: the actual {@link Set} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param name the name to retrieve. - * @param elementsClass the class for the elements of the set to retrieve. - * @return the value of the {@code i}th element as a set of {@code T} objects. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a set. - */ - public Set getSet(String name, Class elementsClass); - - /** - * Returns the value for {@code name} as a set. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a set of the specified type. - * - *

Use this variant with nested collections, which produce a generic element type: - * - *

-   * {@code Set> l = row.getSet("theColumn", new TypeToken>() {});}
-   * 
- * - *

Implementation note: the actual {@link Set} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param name the name to retrieve. - * @param elementsType the type for the elements of the set to retrieve. - * @return the value of the {@code i}th element as a set of {@code T} objects. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a set. - */ - public Set getSet(String name, TypeToken elementsType); - - /** - * Returns the value for {@code name} as a map. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a map of the specified types. - * - *

If the type of the keys and/or values is generic, use {@link #getMap(String, TypeToken, - * TypeToken)}. - * - *

Implementation note: the actual {@link Map} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param name the name to retrieve. - * @param keysClass the class for the keys of the map to retrieve. - * @param valuesClass the class for the values of the map to retrieve. - * @return the value of {@code name} as a map of {@code K} to {@code V} objects. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a map. - */ - public Map getMap(String name, Class keysClass, Class valuesClass); - - /** - * Returns the value for {@code name} as a map. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a map of the specified types. - * - *

Use this variant with nested collections, which produce a generic element type: - * - *

-   * {@code Map> l = row.getMap("theColumn", TypeToken.of(Integer.class), new TypeToken>() {});}
-   * 
- * - *

Implementation note: the actual {@link Map} implementation will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent. By default, the driver will return mutable - * instances, and a CQL {@code NULL} will mapped to an empty collection (note that Cassandra makes - * no distinction between {@code NULL} and an empty collection). - * - * @param name the name to retrieve. - * @param keysType the class for the keys of the map to retrieve. - * @param valuesType the class for the values of the map to retrieve. - * @return the value of {@code name} as a map of {@code K} to {@code V} objects. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a map. - */ - public Map getMap(String name, TypeToken keysType, TypeToken valuesType); - - /** - * Return the value for {@code name} as a UDT value. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code UDTValue} (if the CQL type is a UDT, the registry will generate a codec - * automatically). - * - * @param name the name to retrieve. - * @return the value of {@code name} as a UDT value. If the value is NULL, then {@code null} will - * be returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a {@code UDTValue}. - */ - public UDTValue getUDTValue(String name); - - /** - * Return the value for {@code name} as a tuple value. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to a {@code TupleValue} (if the CQL type is a tuple, the registry will generate a codec - * automatically). - * - * @param name the name to retrieve. - * @return the value of {@code name} as a tuple value. If the value is NULL, then {@code null} - * will be returned. - * @throws IllegalArgumentException if {@code name} is not valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to a {@code TupleValue}. - */ - public TupleValue getTupleValue(String name); - - /** - * Returns the value for {@code name} as the Java type matching its CQL type. - * - *

This method uses the {@link CodecRegistry} to find the first codec that handles the - * underlying CQL type. The Java type of the returned object will be determined by the codec that - * was selected. - * - *

Use this method to dynamically inspect elements when types aren't known in advance, for - * instance if you're writing a generic row logger. If you know the target Java type, it is - * generally preferable to use typed getters, such as the ones for built-in types ({@link - * #getBool(String)}, {@link #getInt(String)}, etc.), or {@link #get(String, Class)} and {@link - * #get(String, TypeToken)} for custom types. - * - * @param name the name to retrieve. - * @return the value of {@code name} as the Java type matching its CQL type. If the value is NULL - * and is a simple type, UDT or tuple, {@code null} is returned. If it is NULL and is a - * collection type, an empty (immutable) collection is returned. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @see CodecRegistry#codecFor(DataType) - */ - Object getObject(String name); - - /** - * Returns the value for {@code name} converted to the given Java type. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to the given Java type. - * - *

If the target type is generic, use {@link #get(String, TypeToken)}. - * - *

Implementation note: the actual object returned by this method will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent; by default, a CQL {@code NULL} value translates to - * {@code null} for simple CQL types, UDTs and tuples, and to empty collections for all CQL - * collection types. - * - * @param name the name to retrieve. - * @param targetClass The Java type the value should be converted to. - * @return the value for {@code name} value converted to the given Java type. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to {@code targetClass}. - */ - T get(String name, Class targetClass); - - /** - * Returns the value for {@code name} converted to the given Java type. - * - *

This method uses the {@link CodecRegistry} to find a codec to convert the underlying CQL - * type to the given Java type. - * - *

Implementation note: the actual object returned by this method will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent; by default, a CQL {@code NULL} value translates to - * {@code null} for simple CQL types, UDTs and tuples, and to empty collections for all CQL - * collection types. - * - * @param name the name to retrieve. - * @param targetType The Java type the value should be converted to. - * @return the value for {@code name} value converted to the given Java type. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the underlying CQL - * type to {@code targetType}. - */ - T get(String name, TypeToken targetType); - - /** - * Returns the value for {@code name} converted using the given {@link TypeCodec}. - * - *

This method entirely bypasses the {@link CodecRegistry} and forces the driver to use the - * given codec instead. This can be useful if the codec would collide with a previously registered - * one, or if you want to use the codec just once without registering it. - * - *

It is the caller's responsibility to ensure that the given codec {@link - * TypeCodec#accepts(DataType) accepts} the underlying CQL type; failing to do so may result in - * {@link InvalidTypeException}s being thrown. - * - *

Implementation note: the actual object returned by this method will depend on the {@link - * TypeCodec codec} being used; therefore, callers should make no assumptions concerning its - * mutability nor its thread-safety. Furthermore, the behavior of this method in respect to CQL - * {@code NULL} values is also codec-dependent; by default, a CQL {@code NULL} value translates to - * {@code null} for simple CQL types, UDTs and tuples, and to empty collections for all CQL - * collection types. - * - * @param name the name to retrieve. - * @param codec The {@link TypeCodec} to use to deserialize the value; may not be {@code null}. - * @return the value of the {@code i}th value converted using the given {@link TypeCodec}. - * @throws InvalidTypeException if the given codec does not {@link TypeCodec#accepts(DataType) - * accept} the underlying CQL type. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - */ - T get(String name, TypeCodec codec); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/GettableData.java b/driver-core/src/main/java/com/datastax/driver/core/GettableData.java deleted file mode 100644 index e52aa6ca1fc..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/GettableData.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Collection of (typed) CQL values that can be retrieved either by index (starting at zero) or by - * name. - */ -public interface GettableData extends GettableByIndexData, GettableByNameData {} diff --git a/driver-core/src/main/java/com/datastax/driver/core/GuavaCompatibility.java b/driver-core/src/main/java/com/datastax/driver/core/GuavaCompatibility.java deleted file mode 100644 index 069f550b049..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/GuavaCompatibility.java +++ /dev/null @@ -1,361 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.google.common.base.Function; -import com.google.common.collect.BiMap; -import com.google.common.collect.Maps; -import com.google.common.net.HostAndPort; -import com.google.common.reflect.TypeToken; -import com.google.common.util.concurrent.AsyncFunction; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.Map; -import java.util.concurrent.Executor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A compatibility layer to support a wide range of Guava versions. - * - *

The driver is compatible with Guava 16.0.1 or higher, but Guava 20 introduced incompatible - * breaking changes in its API, that could in turn be breaking for legacy driver clients if we - * simply upgraded our dependency. We don't want to increment our major version "just" for Guava (we - * have other changes planned). - * - *

Therefore we depend on Guava 19, which has both the deprecated and the new APIs, and detect - * the actual version at runtime in order to call the relevant methods. - * - *

This is a hack, and might not work with subsequent Guava releases; the real fix is to stop - * exposing Guava in our public API. We'll address that in version 4 of the driver. - */ -@SuppressWarnings("deprecation") -public abstract class GuavaCompatibility { - - private static final Logger logger = LoggerFactory.getLogger(GuavaCompatibility.class); - - /** - * The unique instance of this class, that is compatible with the Guava version found in the - * classpath. - */ - public static final GuavaCompatibility INSTANCE = selectImplementation(); - - /** - * Force the initialization of the class. This should be called early to ensure a fast failure if - * an incompatible version of Guava is in the classpath (the driver code calls it when loading the - * {@link Cluster} class). - */ - public static void init() { - // nothing to do, we just want the static initializers to run - } - - /** - * Returns a {@code Future} whose result is taken from the given primary {@code input} or, if the - * primary input fails, from the {@code Future} provided by the {@code fallback}. - * - * @see Futures#withFallback(ListenableFuture, com.google.common.util.concurrent.FutureFallback) - * @see Futures#catchingAsync(ListenableFuture, Class, AsyncFunction) - */ - public abstract ListenableFuture withFallback( - ListenableFuture input, AsyncFunction fallback); - - /** - * Returns a {@code Future} whose result is taken from the given primary {@code input} or, if the - * primary input fails, from the {@code Future} provided by the {@code fallback}. - * - * @see Futures#withFallback(ListenableFuture, com.google.common.util.concurrent.FutureFallback, - * Executor) - * @see Futures#catchingAsync(ListenableFuture, Class, AsyncFunction, Executor) - */ - public abstract ListenableFuture withFallback( - ListenableFuture input, AsyncFunction fallback, Executor executor); - - /** - * Registers separate success and failure callbacks to be run when the {@code Future}'s - * computation is {@linkplain java.util.concurrent.Future#isDone() complete} or, if the - * computation is already complete, immediately. - * - *

The callback is run in {@link #sameThreadExecutor()}. - * - * @see Futures#addCallback(ListenableFuture, FutureCallback, Executor) - */ - public void addCallback(ListenableFuture input, FutureCallback callback) { - addCallback(input, callback, sameThreadExecutor()); - } - - /** - * Registers separate success and failure callbacks to be run when the {@code Future}'s - * computation is {@linkplain java.util.concurrent.Future#isDone() complete} or, if the - * computation is already complete, immediately. - * - * @see Futures#addCallback(ListenableFuture, FutureCallback, Executor) - */ - public void addCallback( - ListenableFuture input, FutureCallback callback, Executor executor) { - Futures.addCallback(input, callback, executor); - } - - /** - * Returns a new {@code ListenableFuture} whose result is the product of applying the given {@code - * Function} to the result of the given {@code Future}. - * - *

The callback is run in {@link #sameThreadExecutor()}. - * - * @see Futures#transform(ListenableFuture, Function, Executor) - */ - public ListenableFuture transform( - ListenableFuture input, Function function) { - return transform(input, function, sameThreadExecutor()); - } - - /** - * Returns a new {@code ListenableFuture} whose result is the product of applying the given {@code - * Function} to the result of the given {@code Future}. - * - * @see Futures#transform(ListenableFuture, Function, Executor) - */ - public ListenableFuture transform( - ListenableFuture input, Function function, Executor executor) { - return Futures.transform(input, function, executor); - } - - /** - * Returns a new {@code ListenableFuture} whose result is asynchronously derived from the result - * of the given {@code Future}. More precisely, the returned {@code Future} takes its result from - * a {@code Future} produced by applying the given {@code AsyncFunction} to the result of the - * original {@code Future}. - * - * @see Futures#transform(ListenableFuture, AsyncFunction) - * @see Futures#transformAsync(ListenableFuture, AsyncFunction) - */ - public abstract ListenableFuture transformAsync( - ListenableFuture input, AsyncFunction function); - - /** - * Returns a new {@code ListenableFuture} whose result is asynchronously derived from the result - * of the given {@code Future}. More precisely, the returned {@code Future} takes its result from - * a {@code Future} produced by applying the given {@code AsyncFunction} to the result of the - * original {@code Future}. - * - * @see Futures#transform(ListenableFuture, AsyncFunction, Executor) - * @see Futures#transformAsync(ListenableFuture, AsyncFunction, Executor) - */ - public abstract ListenableFuture transformAsync( - ListenableFuture input, AsyncFunction function, Executor executor); - - /** - * Returns true if {@code target} is a supertype of {@code argument}. "Supertype" is defined - * according to the rules for type arguments introduced with Java generics. - * - * @see TypeToken#isAssignableFrom(Type) - * @see TypeToken#isSupertypeOf(Type) - */ - public abstract boolean isSupertypeOf(TypeToken target, TypeToken argument); - - /** - * Returns an {@link Executor} that runs each task in the thread that invokes {@link - * Executor#execute execute}, as in {@link - * java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy}. - * - * @see MoreExecutors#sameThreadExecutor() - * @see MoreExecutors#directExecutor() - */ - public abstract Executor sameThreadExecutor(); - - /** - * Returns the portion of the given {@link HostAndPort} instance that should represent the - * hostname or IPv4/IPv6 literal. - * - *

The method {@code HostAndPort.getHostText} has been replaced with {@code - * HostAndPort.getHost} starting with Guava 20.0; it has been completely removed in Guava 22.0. - */ - @SuppressWarnings("JavaReflectionMemberAccess") - public String getHost(HostAndPort hostAndPort) { - try { - // Guava >= 20.0 - return (String) HostAndPort.class.getMethod("getHost").invoke(hostAndPort); - } catch (Exception e) { - // Guava < 22.0 - return hostAndPort.getHostText(); - } - } - - private static GuavaCompatibility selectImplementation() { - if (isGuava_19_0_OrHigher()) { - logger.info("Detected Guava >= 19 in the classpath, using modern compatibility layer"); - return new Version19OrHigher(); - } else if (isGuava_16_0_1_OrHigher()) { - logger.info("Detected Guava < 19 in the classpath, using legacy compatibility layer"); - return new Version18OrLower(); - } else { - throw new DriverInternalError( - "Detected incompatible version of Guava in the classpath. " - + "You need 16.0.1 or higher."); - } - } - - private static class Version18OrLower extends GuavaCompatibility { - - @Override - public ListenableFuture withFallback( - ListenableFuture input, final AsyncFunction fallback) { - return Futures.withFallback( - input, - new com.google.common.util.concurrent.FutureFallback() { - @Override - public ListenableFuture create(Throwable t) throws Exception { - return fallback.apply(t); - } - }); - } - - @Override - public ListenableFuture withFallback( - ListenableFuture input, - final AsyncFunction fallback, - Executor executor) { - return Futures.withFallback( - input, - new com.google.common.util.concurrent.FutureFallback() { - @Override - public ListenableFuture create(Throwable t) throws Exception { - return fallback.apply(t); - } - }, - executor); - } - - @Override - public ListenableFuture transformAsync( - ListenableFuture input, AsyncFunction function) { - return Futures.transform(input, function); - } - - @Override - public ListenableFuture transformAsync( - ListenableFuture input, - AsyncFunction function, - Executor executor) { - return Futures.transform(input, function, executor); - } - - @Override - public boolean isSupertypeOf(TypeToken target, TypeToken argument) { - return target.isAssignableFrom(argument); - } - - @Override - public Executor sameThreadExecutor() { - return MoreExecutors.sameThreadExecutor(); - } - } - - private static class Version19OrHigher extends GuavaCompatibility { - - @Override - public ListenableFuture withFallback( - ListenableFuture input, AsyncFunction fallback) { - return withFallback(input, fallback, sameThreadExecutor()); - } - - @Override - public ListenableFuture withFallback( - ListenableFuture input, - AsyncFunction fallback, - Executor executor) { - return Futures.catchingAsync(input, Throwable.class, fallback, executor); - } - - @Override - public ListenableFuture transformAsync( - ListenableFuture input, AsyncFunction function) { - return transformAsync(input, function, sameThreadExecutor()); - } - - @Override - public ListenableFuture transformAsync( - ListenableFuture input, - AsyncFunction function, - Executor executor) { - return Futures.transformAsync(input, function, executor); - } - - @Override - public boolean isSupertypeOf(TypeToken target, TypeToken argument) { - return target.isSupertypeOf(argument); - } - - @Override - public Executor sameThreadExecutor() { - return MoreExecutors.directExecutor(); - } - } - - private static boolean isGuava_19_0_OrHigher() { - return methodExists( - Futures.class, - "transformAsync", - ListenableFuture.class, - AsyncFunction.class, - Executor.class); - } - - private static boolean isGuava_16_0_1_OrHigher() { - // Cheap check for < 16.0 - if (!methodExists(Maps.class, "asConverter", BiMap.class)) { - return false; - } - // More elaborate check to filter out 16.0, which has a bug in TypeToken. We need 16.0.1. - boolean resolved = false; - TypeToken> mapOfString = TypeTokens.mapOf(String.class, String.class); - Type type = mapOfString.getType(); - if (type instanceof ParameterizedType) { - ParameterizedType pType = (ParameterizedType) type; - Type[] types = pType.getActualTypeArguments(); - if (types.length == 2) { - TypeToken valueType = TypeToken.of(types[1]); - resolved = valueType.getRawType().equals(String.class); - } - } - if (!resolved) { - logger.debug( - "Detected Guava issue #1635 which indicates that version 16.0 is in the classpath"); - } - return resolved; - } - - private static boolean methodExists( - Class declaringClass, String methodName, Class... parameterTypes) { - try { - declaringClass.getMethod(methodName, parameterTypes); - return true; - } catch (Exception e) { - logger.debug( - "Error while checking existence of method " - + declaringClass.getSimpleName() - + "." - + methodName, - e); - return false; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Host.java b/driver-core/src/main/java/com/datastax/driver/core/Host.java deleted file mode 100644 index e044cf5fea1..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Host.java +++ /dev/null @@ -1,598 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.util.concurrent.ListenableFuture; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.ReentrantLock; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A Cassandra node. - * - *

This class keeps the information the driver maintain on a given Cassandra node. - */ -public class Host { - - private static final Logger logger = LoggerFactory.getLogger(Host.class); - - static final Logger statesLogger = LoggerFactory.getLogger(Host.class.getName() + ".STATES"); - - // The address we'll use to connect to the node - private final EndPoint endPoint; - - // The broadcast RPC address, as reported in system tables. - // Note that, unlike previous versions of the driver, this address is NOT TRANSLATED. - private volatile InetSocketAddress broadcastRpcAddress; - - // The broadcast_address as known by Cassandra. - // We use that internally because - // that's the 'peer' in the 'System.peers' table and avoids querying the full peers table in - // ControlConnection.refreshNodeInfo. - private volatile InetSocketAddress broadcastSocketAddress; - - // The listen_address as known by Cassandra. - // This is usually the same as broadcast_address unless - // specified otherwise in cassandra.yaml file. - private volatile InetSocketAddress listenSocketAddress; - - private volatile UUID hostId; - - private volatile UUID schemaVersion; - - enum State { - ADDED, - DOWN, - UP - } - - volatile State state; - /** Ensures state change notifications for that host are handled serially */ - final ReentrantLock notificationsLock = new ReentrantLock(true); - - final ConvictionPolicy convictionPolicy; - private final Cluster.Manager manager; - - // Tracks later reconnection attempts to that host so we avoid adding multiple tasks. - final AtomicReference> reconnectionAttempt = - new AtomicReference>(); - - final ExecutionInfo defaultExecutionInfo; - - private volatile String datacenter; - private volatile String rack; - private volatile VersionNumber cassandraVersion; - - private volatile Set tokens; - - private volatile String dseWorkload; - private volatile boolean dseGraphEnabled; - private volatile VersionNumber dseVersion; - - Host( - EndPoint endPoint, - ConvictionPolicy.Factory convictionPolicyFactory, - Cluster.Manager manager) { - if (endPoint == null || convictionPolicyFactory == null) throw new NullPointerException(); - - this.endPoint = endPoint; - this.convictionPolicy = convictionPolicyFactory.create(this, manager.reconnectionPolicy()); - this.manager = manager; - this.defaultExecutionInfo = new ExecutionInfo(this); - this.state = State.ADDED; - } - - void setLocationInfo(String datacenter, String rack) { - this.datacenter = datacenter; - this.rack = rack; - } - - void setVersion(String cassandraVersion) { - VersionNumber versionNumber = null; - try { - if (cassandraVersion != null) { - versionNumber = VersionNumber.parse(cassandraVersion); - } - } catch (IllegalArgumentException e) { - logger.warn( - "Error parsing Cassandra version {}. This shouldn't have happened", cassandraVersion); - } - this.cassandraVersion = versionNumber; - } - - void setBroadcastRpcAddress(InetSocketAddress broadcastRpcAddress) { - this.broadcastRpcAddress = broadcastRpcAddress; - } - - void setBroadcastSocketAddress(InetSocketAddress broadcastAddress) { - this.broadcastSocketAddress = broadcastAddress; - } - - void setListenSocketAddress(InetSocketAddress listenAddress) { - this.listenSocketAddress = listenAddress; - } - - void setDseVersion(String dseVersion) { - VersionNumber versionNumber = null; - try { - if (dseVersion != null) { - versionNumber = VersionNumber.parse(dseVersion); - } - } catch (IllegalArgumentException e) { - logger.warn("Error parsing DSE version {}. This shouldn't have happened", dseVersion); - } - this.dseVersion = versionNumber; - } - - void setDseWorkload(String dseWorkload) { - this.dseWorkload = dseWorkload; - } - - void setDseGraphEnabled(boolean dseGraphEnabled) { - this.dseGraphEnabled = dseGraphEnabled; - } - - void setHostId(UUID hostId) { - this.hostId = hostId; - } - - void setSchemaVersion(UUID schemaVersion) { - this.schemaVersion = schemaVersion; - } - - boolean supports(ProtocolVersion version) { - return getCassandraVersion() == null - || version.minCassandraVersion().compareTo(getCassandraVersion().nextStable()) <= 0; - } - - /** Returns information to connect to the node. */ - public EndPoint getEndPoint() { - return endPoint; - } - - /** - * Returns the address that the driver will use to connect to the node. - * - * @deprecated This is exposed mainly for historical reasons. Internally, the driver uses {@link - * #getEndPoint()} to establish connections. This is a shortcut for {@code - * getEndPoint().resolve().getAddress()}. - */ - @Deprecated - public InetAddress getAddress() { - return endPoint.resolve().getAddress(); - } - - /** - * Returns the address and port that the driver will use to connect to the node. - * - * @deprecated This is exposed mainly for historical reasons. Internally, the driver uses {@link - * #getEndPoint()} to establish connections. This is a shortcut for {@code - * getEndPoint().resolve()}. - * @see The - * cassandra.yaml configuration file - */ - @Deprecated - public InetSocketAddress getSocketAddress() { - return endPoint.resolve(); - } - - /** - * Returns the broadcast RPC address, as reported by the node. - * - *

This is address reported in {@code system.peers.rpc_address} (Cassandra 3) or {@code - * system.peers_v2.native_address/native_port} (Cassandra 4+). - * - *

Note that this is not necessarily the address that the driver will use to connect: if the - * node is accessed through a proxy, a translation might be necessary; this is handled by {@link - * #getEndPoint()}. - * - *

For versions of Cassandra less than 2.0.16, 2.1.6 or 2.2.0-rc1, this will be {@code null} - * for the control host. It will get updated if the control connection switches to another host. - * - * @see CASSANDRA-9436 (where the - * information was added for the control host) - */ - public InetSocketAddress getBroadcastRpcAddress() { - return broadcastRpcAddress; - } - - /** - * Returns the node broadcast address, if known. Otherwise {@code null}. - * - *

This is a shortcut for {@code getBroadcastSocketAddress().getAddress()}. - * - * @return the node broadcast address, if known. Otherwise {@code null}. - * @see #getBroadcastSocketAddress() - * @see The - * cassandra.yaml configuration file - */ - public InetAddress getBroadcastAddress() { - return broadcastSocketAddress != null ? broadcastSocketAddress.getAddress() : null; - } - - /** - * Returns the node broadcast address (that is, the address by which it should be contacted by - * other peers in the cluster), if known. Otherwise {@code null}. - * - *

Note that the port of the returned address will be 0 for versions of Cassandra older than - * 4.0. - * - *

This corresponds to the {@code broadcast_address} cassandra.yaml file setting and is by - * default the same as {@link #getListenSocketAddress()}, unless specified otherwise in - * cassandra.yaml. This is NOT the address clients should use to contact this node. - * - *

This information is always available for peer hosts. For the control host, it's only - * available if CASSANDRA-9436 is fixed on the server side (Cassandra versions >= 2.0.16, 2.1.6, - * 2.2.0 rc1). For older versions, note that if the driver loses the control connection and - * reconnects to a different control host, the old control host becomes a peer, and therefore its - * broadcast address is updated. - * - * @return the node broadcast address, if known. Otherwise {@code null}. - * @see The - * cassandra.yaml configuration file - */ - public InetSocketAddress getBroadcastSocketAddress() { - return broadcastSocketAddress; - } - - /** - * Returns the node listen address, if known. Otherwise {@code null}. - * - *

This is a shortcut for {@code getListenSocketAddress().getAddress()}. - * - * @return the node listen address, if known. Otherwise {@code null}. - * @see #getListenSocketAddress() - * @see The - * cassandra.yaml configuration file - */ - public InetAddress getListenAddress() { - return listenSocketAddress != null ? listenSocketAddress.getAddress() : null; - } - - /** - * Returns the node listen address (that is, the address the node uses to contact other peers in - * the cluster), if known. Otherwise {@code null}. - * - *

Note that the port of the returned address will be 0 for versions of Cassandra older than - * 4.0. - * - *

This corresponds to the {@code listen_address} cassandra.yaml file setting. This is NOT - * the address clients should use to contact this node. - * - *

This information is available for the control host if CASSANDRA-9603 is fixed on the server - * side (Cassandra versions >= 2.0.17, 2.1.8, 2.2.0 rc2). It's currently not available for peer - * hosts. Note that the current driver code already tries to read a {@code listen_address} column - * in {@code system.peers}; when a future Cassandra version adds it, it will be picked by the - * driver without any further change needed. - * - * @return the node listen address, if known. Otherwise {@code null}. - * @see The - * cassandra.yaml configuration file - */ - public InetSocketAddress getListenSocketAddress() { - return listenSocketAddress; - } - - /** - * Returns the name of the datacenter this host is part of. - * - *

The returned datacenter name is the one as known by Cassandra. It is also possible for this - * information to be unavailable. In that case this method returns {@code null}, and the caller - * should always be aware of this possibility. - * - * @return the Cassandra datacenter name or null if datacenter is unavailable. - */ - public String getDatacenter() { - return datacenter; - } - - /** - * Returns the name of the rack this host is part of. - * - *

The returned rack name is the one as known by Cassandra. It is also possible for this - * information to be unavailable. In that case this method returns {@code null}, and the caller - * should always be aware of this possibility. - * - * @return the Cassandra rack name or null if the rack is unavailable - */ - public String getRack() { - return rack; - } - - /** - * The Cassandra version the host is running. - * - *

It is also possible for this information to be unavailable. In that case this method returns - * {@code null}, and the caller should always be aware of this possibility. - * - * @return the Cassandra version the host is running. - */ - public VersionNumber getCassandraVersion() { - return cassandraVersion; - } - - /** - * The DSE version the host is running. - * - *

It is also possible for this information to be unavailable. In that case this method returns - * {@code null}, and the caller should always be aware of this possibility. - * - * @return the DSE version the host is running. - * @deprecated Please use the Java driver - * for DSE if you are connecting to a DataStax Enterprise (DSE) cluster. This method might - * not function properly with future versions of DSE. - */ - @Deprecated - public VersionNumber getDseVersion() { - return dseVersion; - } - - /** - * The DSE Workload the host is running. - * - *

It is also possible for this information to be unavailable. In that case this method returns - * {@code null}, and the caller should always be aware of this possibility. - * - * @return the DSE workload the host is running. - * @deprecated Please use the Java driver - * for DSE if you are connecting to a DataStax Enterprise (DSE) cluster. This method might - * not function properly with future versions of DSE. - */ - @Deprecated - public String getDseWorkload() { - return dseWorkload; - } - - /** - * Returns whether the host is running DSE Graph. - * - * @return whether the node is running DSE Graph. - * @deprecated Please use the Java driver - * for DSE if you are connecting to a DataStax Enterprise (DSE) cluster. This method might - * not function properly with future versions of DSE. - */ - @Deprecated - public boolean isDseGraphEnabled() { - return dseGraphEnabled; - } - - /** - * Return the host id value for the host. - * - *

The host id is the main identifier used by Cassandra on the server for internal - * communication (gossip). It is referenced as the column {@code host_id} in the {@code - * system.local} or {@code system.peers} table. - * - * @return the node's host id value. - */ - public UUID getHostId() { - return hostId; - } - - /** - * Return the current schema version for the host. - * - *

Schema versions in Cassandra are used to ensure all the nodes agree on the current Cassandra - * schema when it is modified. For more information see {@link - * ExecutionInfo#isSchemaInAgreement()} - * - * @return the node's current schema version value. - */ - public UUID getSchemaVersion() { - return schemaVersion; - } - - /** - * Returns the tokens that this host owns. - * - * @return the (immutable) set of tokens. - */ - public Set getTokens() { - return tokens; - } - - void setTokens(Set tokens) { - this.tokens = tokens; - } - - /** - * Returns whether the host is considered up by the driver. - * - *

Please note that this is only the view of the driver and may not reflect reality. In - * particular a node can be down but the driver hasn't detected it yet, or it can have been - * restarted and the driver hasn't detected it yet (in particular, for hosts to which the driver - * does not connect (because the {@code LoadBalancingPolicy.distance} method says so), this - * information may be durably inaccurate). This information should thus only be considered as best - * effort and should not be relied upon too strongly. - * - * @return whether the node is considered up. - */ - public boolean isUp() { - return state == State.UP; - } - - /** - * Returns a description of the host's state, as seen by the driver. - * - *

This is exposed for debugging purposes only; the format of this string might change between - * driver versions, so clients should not make any assumptions about it. - * - * @return a description of the host's state. - */ - public String getState() { - return state.name(); - } - - /** - * Returns a {@code ListenableFuture} representing the completion of the reconnection attempts - * scheduled after a host is marked {@code DOWN}. - * - *

If the caller cancels this future, the driver will not try to reconnect to this host - * until it receives an UP event for it. Note that this could mean never, if the node was marked - * down because of a driver-side error (e.g. read timeout) but no failure was detected by - * Cassandra. The caller might decide to trigger an explicit reconnection attempt at a later point - * with {@link #tryReconnectOnce()}. - * - * @return the future, or {@code null} if no reconnection attempt was in progress. - */ - public ListenableFuture getReconnectionAttemptFuture() { - return reconnectionAttempt.get(); - } - - /** - * Triggers an asynchronous reconnection attempt to this host. - * - *

This method is intended for load balancing policies that mark hosts as {@link - * HostDistance#IGNORED IGNORED}, but still need a way to periodically check these hosts' states - * (UP / DOWN). - * - *

For a host that is at distance {@code IGNORED}, this method will try to reconnect exactly - * once: if reconnection succeeds, the host is marked {@code UP}; otherwise, no further attempts - * will be scheduled. It has no effect if the node is already {@code UP}, or if a reconnection - * attempt is already in progress. - * - *

Note that if the host is not a distance {@code IGNORED}, this method will - * trigger a periodic reconnection attempt if the reconnection fails. - */ - public void tryReconnectOnce() { - this.manager.startSingleReconnectionAttempt(this); - } - - @Override - public boolean equals(Object other) { - if (other instanceof Host) { - Host that = (Host) other; - return this.endPoint.equals(that.endPoint); - } - return false; - } - - @Override - public int hashCode() { - return endPoint.hashCode(); - } - - boolean wasJustAdded() { - return state == State.ADDED; - } - - @Override - public String toString() { - return endPoint.toString(); - } - - void setDown() { - state = State.DOWN; - } - - void setUp() { - state = State.UP; - } - - /** - * Interface for listeners that are interested in hosts added, up, down and removed events. - * - *

It is possible for the same event to be fired multiple times, particularly for up or down - * events. Therefore, a listener should ignore the same event if it has already been notified of a - * node's state. - */ - public interface StateListener { - - /** - * Called when a new node is added to the cluster. - * - *

The newly added node should be considered up. - * - * @param host the host that has been newly added. - */ - void onAdd(Host host); - - /** - * Called when a node is determined to be up. - * - * @param host the host that has been detected up. - */ - void onUp(Host host); - - /** - * Called when a node is determined to be down. - * - * @param host the host that has been detected down. - */ - void onDown(Host host); - - /** - * Called when a node is removed from the cluster. - * - * @param host the removed host. - */ - void onRemove(Host host); - - /** - * Gets invoked when the tracker is registered with a cluster, or at cluster startup if the - * tracker was registered at initialization with {@link - * com.datastax.driver.core.Cluster.Initializer#register(LatencyTracker)}. - * - * @param cluster the cluster that this tracker is registered with. - */ - void onRegister(Cluster cluster); - - /** - * Gets invoked when the tracker is unregistered from a cluster, or at cluster shutdown if the - * tracker was not unregistered. - * - * @param cluster the cluster that this tracker was registered with. - */ - void onUnregister(Cluster cluster); - } - - /** - * A {@code StateListener} that tracks when it gets registered or unregistered with a cluster. - * - *

This interface exists only for backward-compatibility reasons: starting with the 3.0 branch - * of the driver, its methods are on the parent interface directly. - */ - public interface LifecycleAwareStateListener extends StateListener { - /** - * Gets invoked when the listener is registered with a cluster, or at cluster startup if the - * listener was registered at initialization with {@link - * com.datastax.driver.core.Cluster#register(Host.StateListener)}. - * - * @param cluster the cluster that this listener is registered with. - */ - @Override - void onRegister(Cluster cluster); - - /** - * Gets invoked when the listener is unregistered from a cluster, or at cluster shutdown if the - * listener was not unregistered. - * - * @param cluster the cluster that this listener was registered with. - */ - @Override - void onUnregister(Cluster cluster); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/HostConnectionPool.java b/driver-core/src/main/java/com/datastax/driver/core/HostConnectionPool.java deleted file mode 100644 index ed6631ec4bf..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/HostConnectionPool.java +++ /dev/null @@ -1,762 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.Connection.State.GONE; -import static com.datastax.driver.core.Connection.State.OPEN; -import static com.datastax.driver.core.Connection.State.RESURRECTING; -import static com.datastax.driver.core.Connection.State.TRASHED; - -import com.datastax.driver.core.exceptions.AuthenticationException; -import com.datastax.driver.core.exceptions.BusyPoolException; -import com.datastax.driver.core.exceptions.ConnectionException; -import com.datastax.driver.core.exceptions.UnsupportedProtocolVersionException; -import com.datastax.driver.core.utils.MoreFutures; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Throwables; -import com.google.common.collect.Lists; -import com.google.common.util.concurrent.AsyncFunction; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import com.google.common.util.concurrent.Uninterruptibles; -import io.netty.util.concurrent.EventExecutor; -import java.util.ArrayList; -import java.util.List; -import java.util.ListIterator; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CopyOnWriteArraySet; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -class HostConnectionPool implements Connection.Owner { - - private static final Logger logger = LoggerFactory.getLogger(HostConnectionPool.class); - - private static final int MAX_SIMULTANEOUS_CREATION = 1; - - final Host host; - volatile HostDistance hostDistance; - protected final SessionManager manager; - - final List connections; - private final AtomicInteger open; - /** The total number of in-flight requests on all connections of this pool. */ - final AtomicInteger totalInFlight = new AtomicInteger(); - /** - * The maximum value of {@link #totalInFlight} since the last call to {@link - * #cleanupIdleConnections(long)} - */ - private final AtomicInteger maxTotalInFlight = new AtomicInteger(); - - @VisibleForTesting final Set trash = new CopyOnWriteArraySet(); - - private final Queue pendingBorrows = new ConcurrentLinkedQueue(); - final AtomicInteger pendingBorrowCount = new AtomicInteger(); - - private final Runnable newConnectionTask; - - private final AtomicInteger scheduledForCreation = new AtomicInteger(); - - private final EventExecutor timeoutsExecutor; - - private final AtomicReference closeFuture = new AtomicReference(); - - private enum Phase { - INITIALIZING, - READY, - INIT_FAILED, - CLOSING - } - - protected final AtomicReference phase = new AtomicReference(Phase.INITIALIZING); - - // When a request times out, we may never release its stream ID. So over time, a given connection - // may get less an less available streams. When the number of available ones go below the - // following threshold, we just replace the connection by a new one. - private final int minAllowedStreams; - - HostConnectionPool(Host host, HostDistance hostDistance, SessionManager manager) { - assert hostDistance != HostDistance.IGNORED; - this.host = host; - this.hostDistance = hostDistance; - this.manager = manager; - - this.newConnectionTask = - new Runnable() { - @Override - public void run() { - addConnectionIfUnderMaximum(); - scheduledForCreation.decrementAndGet(); - } - }; - - this.connections = new CopyOnWriteArrayList(); - this.open = new AtomicInteger(); - - this.minAllowedStreams = options().getMaxRequestsPerConnection(hostDistance) * 3 / 4; - - this.timeoutsExecutor = manager.getCluster().manager.connectionFactory.eventLoopGroup.next(); - } - - /** - * @param reusedConnection an existing connection (from a reconnection attempt) that we want to - * reuse as part of this pool. Might be null or already used by another pool. - */ - ListenableFuture initAsync(Connection reusedConnection) { - Executor initExecutor = - manager.cluster.manager.configuration.getPoolingOptions().getInitializationExecutor(); - - // Create initial core connections - final int coreSize = options().getCoreConnectionsPerHost(hostDistance); - final List connections = Lists.newArrayListWithCapacity(coreSize); - final List> connectionFutures = Lists.newArrayListWithCapacity(coreSize); - - int toCreate = coreSize; - - if (reusedConnection != null && toCreate > 0 && reusedConnection.setOwner(this)) { - toCreate -= 1; - connections.add(reusedConnection); - connectionFutures.add(MoreFutures.VOID_SUCCESS); - } - - List newConnections = manager.connectionFactory().newConnections(this, toCreate); - connections.addAll(newConnections); - for (Connection connection : newConnections) { - ListenableFuture connectionFuture = connection.initAsync(); - connectionFutures.add(handleErrors(connectionFuture, initExecutor)); - } - - ListenableFuture> allConnectionsFuture = Futures.allAsList(connectionFutures); - - final SettableFuture initFuture = SettableFuture.create(); - GuavaCompatibility.INSTANCE.addCallback( - allConnectionsFuture, - new FutureCallback>() { - @Override - public void onSuccess(List l) { - // Some of the connections might have failed, keep only the successful ones - ListIterator it = connections.listIterator(); - while (it.hasNext()) { - if (it.next().isClosed()) it.remove(); - } - - HostConnectionPool.this.connections.addAll(connections); - open.set(connections.size()); - - if (isClosed()) { - initFuture.setException( - new ConnectionException( - host.getEndPoint(), "Pool was closed during initialization")); - // we're not sure if closeAsync() saw the connections, so ensure they get closed - forceClose(connections); - } else { - logger.debug( - "Created connection pool to host {} ({} connections needed, {} successfully opened)", - host, - coreSize, - connections.size()); - phase.compareAndSet(Phase.INITIALIZING, Phase.READY); - initFuture.set(null); - } - } - - @Override - public void onFailure(Throwable t) { - phase.compareAndSet(Phase.INITIALIZING, Phase.INIT_FAILED); - forceClose(connections); - initFuture.setException(t); - } - }, - initExecutor); - return initFuture; - } - - private ListenableFuture handleErrors( - ListenableFuture connectionInitFuture, Executor executor) { - return GuavaCompatibility.INSTANCE.withFallback( - connectionInitFuture, - new AsyncFunction() { - @Override - public ListenableFuture apply(Throwable t) throws Exception { - // Propagate these exceptions because they mean no connection will ever succeed. They - // will be handled - // accordingly in SessionManager#maybeAddPool. - Throwables.propagateIfInstanceOf(t, ClusterNameMismatchException.class); - Throwables.propagateIfInstanceOf(t, UnsupportedProtocolVersionException.class); - Throwables.propagateIfInstanceOf(t, AuthenticationException.class); - - // We don't want to swallow Errors either as they probably indicate a more serious issue - // (OOME...) - Throwables.propagateIfInstanceOf(t, Error.class); - - // Otherwise, log the exception but return success. - // The pool will simply ignore this connection when it sees that it's been closed. - logger.warn("Error creating connection to " + host, t); - return MoreFutures.VOID_SUCCESS; - } - }, - executor); - } - - // Clean up if we got a fatal error at construction time but still created part of the core - // connections - private void forceClose(List connections) { - for (Connection connection : connections) { - connection.closeAsync().force(); - } - } - - private PoolingOptions options() { - return manager.configuration().getPoolingOptions(); - } - - ListenableFuture borrowConnection(long timeout, TimeUnit unit, int maxQueueSize) { - Phase phase = this.phase.get(); - if (phase != Phase.READY) - return Futures.immediateFailedFuture( - new ConnectionException(host.getEndPoint(), "Pool is " + phase)); - - if (connections.isEmpty()) { - if (host.convictionPolicy.canReconnectNow()) { - int coreSize = options().getCoreConnectionsPerHost(hostDistance); - if (coreSize == 0) { - maybeSpawnNewConnection(); - } else if (scheduledForCreation.compareAndSet(0, coreSize)) { - for (int i = 0; i < coreSize; i++) { - // We don't respect MAX_SIMULTANEOUS_CREATION here because it's only to - // protect against creating connection in excess of core too quickly - manager.blockingExecutor().submit(newConnectionTask); - } - } - return enqueue(timeout, unit, maxQueueSize); - } - } - - int minInFlight = Integer.MAX_VALUE; - Connection leastBusy = null; - for (Connection connection : connections) { - int inFlight = connection.inFlight.get(); - if (inFlight < minInFlight) { - minInFlight = inFlight; - leastBusy = connection; - } - } - - if (leastBusy == null) { - // We could have raced with a shutdown since the last check - if (isClosed()) - return Futures.immediateFailedFuture( - new ConnectionException(host.getEndPoint(), "Pool is shutdown")); - // This might maybe happen if the number of core connections per host is 0 and a connection - // was trashed between - // the previous check to connections and now. But in that case, the line above will have - // trigger the creation of - // a new connection, so just wait that connection and move on - return enqueue(timeout, unit, maxQueueSize); - } else { - while (true) { - int inFlight = leastBusy.inFlight.get(); - - if (inFlight - >= Math.min( - leastBusy.maxAvailableStreams(), - options().getMaxRequestsPerConnection(hostDistance))) { - return enqueue(timeout, unit, maxQueueSize); - } - - if (leastBusy.inFlight.compareAndSet(inFlight, inFlight + 1)) break; - } - } - - int totalInFlightCount = totalInFlight.incrementAndGet(); - // update max atomically: - while (true) { - int oldMax = maxTotalInFlight.get(); - if (totalInFlightCount <= oldMax - || maxTotalInFlight.compareAndSet(oldMax, totalInFlightCount)) break; - } - - int connectionCount = open.get() + scheduledForCreation.get(); - if (connectionCount < options().getCoreConnectionsPerHost(hostDistance)) { - maybeSpawnNewConnection(); - } else if (connectionCount < options().getMaxConnectionsPerHost(hostDistance)) { - // Add a connection if we fill the first n-1 connections and almost fill the last one - int currentCapacity = - (connectionCount - 1) * options().getMaxRequestsPerConnection(hostDistance) - + options().getNewConnectionThreshold(hostDistance); - if (totalInFlightCount > currentCapacity) maybeSpawnNewConnection(); - } - - return leastBusy.setKeyspaceAsync(manager.poolsState.keyspace); - } - - private ListenableFuture enqueue(long timeout, TimeUnit unit, int maxQueueSize) { - if (timeout == 0 || maxQueueSize == 0) { - return Futures.immediateFailedFuture(new BusyPoolException(host.getEndPoint(), 0)); - } - - while (true) { - int count = pendingBorrowCount.get(); - if (count >= maxQueueSize) { - return Futures.immediateFailedFuture( - new BusyPoolException(host.getEndPoint(), maxQueueSize)); - } - if (pendingBorrowCount.compareAndSet(count, count + 1)) { - break; - } - } - - PendingBorrow pendingBorrow = new PendingBorrow(timeout, unit, timeoutsExecutor); - pendingBorrows.add(pendingBorrow); - - // If we raced with shutdown, make sure the future will be completed. This has no effect if it - // was properly - // handled in closeAsync. - if (phase.get() == Phase.CLOSING) { - pendingBorrow.setException(new ConnectionException(host.getEndPoint(), "Pool is shutdown")); - } - - return pendingBorrow.future; - } - - void returnConnection(Connection connection, boolean busy) { - connection.inFlight.decrementAndGet(); - totalInFlight.decrementAndGet(); - - if (isClosed()) { - close(connection); - return; - } - - if (connection.isDefunct()) { - // As part of making it defunct, we have already replaced it or - // closed the pool. - return; - } - - if (connection.state.get() != TRASHED) { - if (connection.maxAvailableStreams() < minAllowedStreams) { - replaceConnection(connection); - } else if (!busy) { - dequeue(connection); - } - } - } - - // When a connection gets returned to the pool, check if there are pending borrows that can be - // completed with it. - private void dequeue(final Connection connection) { - while (!pendingBorrows.isEmpty()) { - - // We can only reuse the connection if it's under its maximum number of inFlight requests. - // Do this atomically, as we could be competing with other borrowConnection or dequeue calls. - while (true) { - int inFlight = connection.inFlight.get(); - if (inFlight - >= Math.min( - connection.maxAvailableStreams(), - options().getMaxRequestsPerConnection(hostDistance))) { - // Connection is full again, stop dequeuing - return; - } - if (connection.inFlight.compareAndSet(inFlight, inFlight + 1)) { - // We acquired the right to reuse the connection for one request, proceed - break; - } - } - - final PendingBorrow pendingBorrow = pendingBorrows.poll(); - if (pendingBorrow == null) { - // Another thread has emptied the queue since our last check, restore the count - connection.inFlight.decrementAndGet(); - } else { - pendingBorrowCount.decrementAndGet(); - // Ensure that the keyspace set on the connection is the one set on the pool state, in the - // general case it will be. - ListenableFuture setKeyspaceFuture = - connection.setKeyspaceAsync(manager.poolsState.keyspace); - // Slight optimization, if the keyspace was already correct the future will be complete, so - // simply complete it here. - if (setKeyspaceFuture.isDone()) { - try { - if (pendingBorrow.set(Uninterruptibles.getUninterruptibly(setKeyspaceFuture))) { - totalInFlight.incrementAndGet(); - } else { - connection.inFlight.decrementAndGet(); - } - } catch (ExecutionException e) { - pendingBorrow.setException(e.getCause()); - connection.inFlight.decrementAndGet(); - } - } else { - // Otherwise the keyspace did need to be set, tie the pendingBorrow future to the set - // keyspace completion. - GuavaCompatibility.INSTANCE.addCallback( - setKeyspaceFuture, - new FutureCallback() { - - @Override - public void onSuccess(Connection c) { - if (pendingBorrow.set(c)) { - totalInFlight.incrementAndGet(); - } else { - connection.inFlight.decrementAndGet(); - } - } - - @Override - public void onFailure(Throwable t) { - pendingBorrow.setException(t); - connection.inFlight.decrementAndGet(); - } - }); - } - } - } - } - - // Trash the connection and create a new one, but we don't call trashConnection - // directly because we want to make sure the connection is always trashed. - private void replaceConnection(Connection connection) { - if (!connection.state.compareAndSet(OPEN, TRASHED)) return; - open.decrementAndGet(); - maybeSpawnNewConnection(); - connection.maxIdleTime = Long.MIN_VALUE; - doTrashConnection(connection); - } - - private boolean trashConnection(Connection connection) { - if (!connection.state.compareAndSet(OPEN, TRASHED)) return true; - - // First, make sure we don't go below core connections - for (; ; ) { - int opened = open.get(); - if (opened <= options().getCoreConnectionsPerHost(hostDistance)) { - connection.state.set(OPEN); - return false; - } - - if (open.compareAndSet(opened, opened - 1)) break; - } - logger.trace("Trashing {}", connection); - connection.maxIdleTime = System.currentTimeMillis() + options().getIdleTimeoutSeconds() * 1000; - doTrashConnection(connection); - return true; - } - - private void doTrashConnection(Connection connection) { - connections.remove(connection); - trash.add(connection); - } - - private boolean addConnectionIfUnderMaximum() { - - // First, make sure we don't cross the allowed limit of open connections - for (; ; ) { - int opened = open.get(); - if (opened >= options().getMaxConnectionsPerHost(hostDistance)) return false; - - if (open.compareAndSet(opened, opened + 1)) break; - } - - if (phase.get() != Phase.READY) { - open.decrementAndGet(); - return false; - } - - // Now really open the connection - try { - Connection newConnection = tryResurrectFromTrash(); - if (newConnection == null) { - if (!host.convictionPolicy.canReconnectNow()) { - open.decrementAndGet(); - return false; - } - logger.debug("Creating new connection on busy pool to {}", host); - newConnection = manager.connectionFactory().open(this); - newConnection.setKeyspace(manager.poolsState.keyspace); - } - connections.add(newConnection); - - newConnection.state.compareAndSet(RESURRECTING, OPEN); // no-op if it was already OPEN - - // We might have raced with pool shutdown since the last check; ensure the connection gets - // closed in case the pool did not do it. - if (isClosed() && !newConnection.isClosed()) { - close(newConnection); - open.decrementAndGet(); - return false; - } - - dequeue(newConnection); - return true; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - // Skip the open but ignore otherwise - open.decrementAndGet(); - return false; - } catch (ConnectionException e) { - open.decrementAndGet(); - logger.debug("Connection error to {} while creating additional connection", host); - return false; - } catch (AuthenticationException e) { - // This shouldn't really happen in theory - open.decrementAndGet(); - logger.error( - "Authentication error while creating additional connection (error is: {})", - e.getMessage()); - return false; - } catch (UnsupportedProtocolVersionException e) { - // This shouldn't happen since we shouldn't have been able to connect in the first place - open.decrementAndGet(); - logger.error( - "UnsupportedProtocolVersionException error while creating additional connection (error is: {})", - e.getMessage()); - return false; - } catch (ClusterNameMismatchException e) { - open.decrementAndGet(); - logger.error( - "ClusterNameMismatchException error while creating additional connection (error is: {})", - e.getMessage()); - return false; - } - } - - private Connection tryResurrectFromTrash() { - long highestMaxIdleTime = System.currentTimeMillis(); - Connection chosen = null; - - while (true) { - for (Connection connection : trash) - if (connection.maxIdleTime > highestMaxIdleTime - && connection.maxAvailableStreams() > minAllowedStreams) { - chosen = connection; - highestMaxIdleTime = connection.maxIdleTime; - } - - if (chosen == null) return null; - else if (chosen.state.compareAndSet(TRASHED, RESURRECTING)) break; - } - logger.trace("Resurrecting {}", chosen); - trash.remove(chosen); - return chosen; - } - - private void maybeSpawnNewConnection() { - if (isClosed() || !host.convictionPolicy.canReconnectNow()) return; - - while (true) { - int inCreation = scheduledForCreation.get(); - if (inCreation >= MAX_SIMULTANEOUS_CREATION) return; - if (scheduledForCreation.compareAndSet(inCreation, inCreation + 1)) break; - } - - manager.blockingExecutor().submit(newConnectionTask); - } - - @Override - public void onConnectionDefunct(final Connection connection) { - if (connection.state.compareAndSet(OPEN, GONE)) open.decrementAndGet(); - connections.remove(connection); - - // Don't try to replace the connection now. Connection.defunct already signaled the failure, - // and either the host will be marked DOWN (which destroys all pools), or we want to prevent - // new connections for some time - } - - void cleanupIdleConnections(long now) { - if (isClosed()) return; - - shrinkIfBelowCapacity(); - cleanupTrash(now); - } - - /** If we have more active connections than needed, trash some of them */ - private void shrinkIfBelowCapacity() { - int currentLoad = maxTotalInFlight.getAndSet(totalInFlight.get()); - - int maxRequestsPerConnection = options().getMaxRequestsPerConnection(hostDistance); - int needed = currentLoad / maxRequestsPerConnection + 1; - if (currentLoad % maxRequestsPerConnection > options().getNewConnectionThreshold(hostDistance)) - needed += 1; - needed = Math.max(needed, options().getCoreConnectionsPerHost(hostDistance)); - int actual = open.get(); - int toTrash = Math.max(0, actual - needed); - - logger.trace( - "Current inFlight = {}, {} connections needed, {} connections available, trashing {}", - currentLoad, - needed, - actual, - toTrash); - - if (toTrash <= 0) return; - - for (Connection connection : connections) - if (trashConnection(connection)) { - toTrash -= 1; - if (toTrash == 0) return; - } - } - - /** Close connections that have been sitting in the trash for too long */ - private void cleanupTrash(long now) { - for (Connection connection : trash) { - if (connection.maxIdleTime < now && connection.state.compareAndSet(TRASHED, GONE)) { - if (connection.inFlight.get() == 0) { - logger.trace("Cleaning up {}", connection); - trash.remove(connection); - close(connection); - } else { - // Given that idleTimeout >> request timeout, all outstanding requests should - // have finished by now, so we should not get here. - // Restore the status so that it's retried on the next cleanup. - connection.state.set(TRASHED); - } - } - } - } - - private void close(final Connection connection) { - connection.closeAsync(); - } - - final boolean isClosed() { - return closeFuture.get() != null; - } - - final CloseFuture closeAsync() { - - CloseFuture future = closeFuture.get(); - if (future != null) return future; - - phase.set(Phase.CLOSING); - - for (PendingBorrow pendingBorrow : pendingBorrows) { - pendingBorrow.setException(new ConnectionException(host.getEndPoint(), "Pool is shutdown")); - } - - future = new CloseFuture.Forwarding(discardAvailableConnections()); - - return closeFuture.compareAndSet(null, future) - ? future - : closeFuture.get(); // We raced, it's ok, return the future that was actually set - } - - int opened() { - return open.get(); - } - - int trashed() { - return trash.size(); - } - - private List discardAvailableConnections() { - // Note: if this gets called before initialization has completed, both connections and trash - // will be empty, - // so this will return an empty list - - List futures = new ArrayList(connections.size() + trash.size()); - - for (final Connection connection : connections) { - CloseFuture future = connection.closeAsync(); - future.addListener( - new Runnable() { - @Override - public void run() { - if (connection.state.compareAndSet(OPEN, GONE)) open.decrementAndGet(); - } - }, - GuavaCompatibility.INSTANCE.sameThreadExecutor()); - futures.add(future); - } - - // Some connections in the trash might still be open if they hadn't reached their idle timeout - for (Connection connection : trash) futures.add(connection.closeAsync()); - - return futures; - } - - // This creates connections if we have less than core connections (if we - // have more than core, connection will just get trash when we can). - void ensureCoreConnections() { - if (isClosed()) return; - - if (!host.convictionPolicy.canReconnectNow()) return; - - // Note: this process is a bit racy, but it doesn't matter since we're still guaranteed to not - // create - // more connection than maximum (and if we create more than core connection due to a race but - // this isn't - // justified by the load, the connection in excess will be quickly trashed anyway) - int opened = open.get(); - for (int i = opened; i < options().getCoreConnectionsPerHost(hostDistance); i++) { - // We don't respect MAX_SIMULTANEOUS_CREATION here because it's only to - // protect against creating connection in excess of core too quickly - scheduledForCreation.incrementAndGet(); - manager.blockingExecutor().submit(newConnectionTask); - } - } - - static class PoolState { - volatile String keyspace; - - void setKeyspace(String keyspace) { - this.keyspace = keyspace; - } - } - - private class PendingBorrow { - final SettableFuture future; - final Future timeoutTask; - - PendingBorrow(final long timeout, final TimeUnit unit, EventExecutor timeoutsExecutor) { - this.future = SettableFuture.create(); - this.timeoutTask = - timeoutsExecutor.schedule( - new Runnable() { - @Override - public void run() { - future.setException(new BusyPoolException(host.getEndPoint(), timeout, unit)); - } - }, - timeout, - unit); - } - - boolean set(Connection connection) { - boolean succeeded = this.future.set(connection); - this.timeoutTask.cancel(false); - return succeeded; - } - - void setException(Throwable exception) { - this.future.setException(exception); - this.timeoutTask.cancel(false); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/HostDistance.java b/driver-core/src/main/java/com/datastax/driver/core/HostDistance.java deleted file mode 100644 index b04d4a7a5b0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/HostDistance.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * The distance to a Cassandra node as assigned by a {@link - * com.datastax.driver.core.policies.LoadBalancingPolicy} (through its {@code distance} method). - * - *

The distance assigned to an host influences how many connections the driver maintains towards - * this host. If for a given host the assigned {@code HostDistance} is {@code LOCAL} or {@code - * REMOTE}, some connections will be maintained by the driver to this host. More active connections - * will be kept to {@code LOCAL} host than to a {@code REMOTE} one (and thus well behaving {@code - * LoadBalancingPolicy} should assign a {@code REMOTE} distance only to hosts that are the less - * often queried). - * - *

However, if a host is assigned the distance {@code IGNORED}, no connection to that host will - * maintained active. In other words, {@code IGNORED} should be assigned to hosts that should not be - * used by this driver (because they are in a remote data center for instance). - */ -public enum HostDistance { - // Note: PoolingOptions rely on the order of the enum. - LOCAL, - REMOTE, - IGNORED -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/IgnoreJDK6Requirement.java b/driver-core/src/main/java/com/datastax/driver/core/IgnoreJDK6Requirement.java deleted file mode 100644 index 9fc5f7b0504..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/IgnoreJDK6Requirement.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Annotation used to mark classes in this project as excluded from JDK 6 signature check performed - * by animal-sniffer - * Maven plugin as they require JDK 8 or higher. - */ -public @interface IgnoreJDK6Requirement {} diff --git a/driver-core/src/main/java/com/datastax/driver/core/InboundTrafficMeter.java b/driver-core/src/main/java/com/datastax/driver/core/InboundTrafficMeter.java deleted file mode 100644 index 872ebac1b1d..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/InboundTrafficMeter.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.codahale.metrics.Meter; -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; - -@Sharable -class InboundTrafficMeter extends ChannelInboundHandlerAdapter { - - private final Meter meter; - - InboundTrafficMeter(Meter meter) { - this.meter = meter; - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof ByteBuf) { - meter.mark(((ByteBuf) msg).readableBytes()); - } - super.channelRead(ctx, msg); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/IndexMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/IndexMetadata.java deleted file mode 100644 index 7a1c9ee86ca..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/IndexMetadata.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.base.Predicate; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; -import java.util.Iterator; -import java.util.Map; - -/** An immutable representation of secondary index metadata. */ -public class IndexMetadata { - - public enum Kind { - KEYS, - CUSTOM, - COMPOSITES - } - - static final String NAME = "index_name"; - - static final String KIND = "kind"; - - static final String OPTIONS = "options"; - - /** The name of the option used to specify the index target (Cassandra 3.0 onwards). */ - public static final String TARGET_OPTION_NAME = "target"; - - /** The name of the option used to specify a custom index class name. */ - public static final String CUSTOM_INDEX_OPTION_NAME = "class_name"; - - /** The name of the option used to specify that the index is on the collection (map) keys. */ - public static final String INDEX_KEYS_OPTION_NAME = "index_keys"; - - /** The name of the option used to specify that the index is on the collection (map) entries. */ - public static final String INDEX_ENTRIES_OPTION_NAME = "index_keys_and_values"; - - private final TableMetadata table; - private final String name; - private final Kind kind; - private final String target; - private final Map options; - - private IndexMetadata( - TableMetadata table, String name, Kind kind, String target, Map options) { - this.table = table; - this.name = name; - this.kind = kind; - this.target = target; - this.options = options; - } - - /** Build an IndexMetadata from a system_schema.indexes row. */ - static IndexMetadata fromRow(TableMetadata table, Row indexRow) { - String name = indexRow.getString(NAME); - Kind kind = Kind.valueOf(indexRow.getString(KIND)); - Map options = indexRow.getMap(OPTIONS, String.class, String.class); - String target = options.get(TARGET_OPTION_NAME); - return new IndexMetadata(table, name, kind, target, options); - } - - /** - * Build an IndexMetadata from a legacy layout (index information is stored along with indexed - * column). - */ - static IndexMetadata fromLegacy(ColumnMetadata column, ColumnMetadata.Raw raw) { - Map indexColumns = raw.indexColumns; - if (indexColumns.isEmpty()) return null; - String type = indexColumns.get(ColumnMetadata.INDEX_TYPE); - if (type == null) return null; - String indexName = indexColumns.get(ColumnMetadata.INDEX_NAME); - String kindStr = indexColumns.get(ColumnMetadata.INDEX_TYPE); - Kind kind = kindStr == null ? null : Kind.valueOf(kindStr); - // Special case check for the value of the index_options column being a string with value 'null' - // as this - // column appears to be set this way (JAVA-834). - String indexOptionsCol = indexColumns.get(ColumnMetadata.INDEX_OPTIONS); - Map options; - if (indexOptionsCol == null || indexOptionsCol.isEmpty() || indexOptionsCol.equals("null")) { - options = ImmutableMap.of(); - } else { - options = SimpleJSONParser.parseStringMap(indexOptionsCol); - } - String target = targetFromLegacyOptions(column, options); - return new IndexMetadata((TableMetadata) column.getParent(), indexName, kind, target, options); - } - - private static String targetFromLegacyOptions( - ColumnMetadata column, Map options) { - String columnName = Metadata.quoteIfNecessary(column.getName()); - if (options.containsKey(INDEX_KEYS_OPTION_NAME)) return String.format("keys(%s)", columnName); - if (options.containsKey(INDEX_ENTRIES_OPTION_NAME)) - return String.format("entries(%s)", columnName); - if (column.getType() instanceof DataType.CollectionType && column.getType().isFrozen()) - return String.format("full(%s)", columnName); - // Note: the keyword 'values' is not accepted as a valid index target function until 3.0 - return columnName; - } - - /** - * Returns the metadata of the table this index is part of. - * - * @return the table this index is part of. - */ - public TableMetadata getTable() { - return table; - } - - /** - * Returns the index name. - * - * @return the index name. - */ - public String getName() { - return name; - } - - /** - * Returns the index kind. - * - * @return the index kind. - */ - public Kind getKind() { - return kind; - } - - /** - * Returns the index target. - * - * @return the index target. - */ - public String getTarget() { - return target; - } - - /** - * Returns whether this index is a custom one. - * - *

If it is indeed a custom index, {@link #getIndexClassName} will return the name of the class - * used in Cassandra to implement that index. - * - * @return {@code true} if this metadata represents a custom index. - */ - public boolean isCustomIndex() { - return getIndexClassName() != null; - } - - /** - * The name of the class used to implement the custom index, if it is one. - * - * @return the name of the class used Cassandra side to implement this custom index if {@code - * isCustomIndex() == true}, {@code null} otherwise. - */ - public String getIndexClassName() { - return getOption(CUSTOM_INDEX_OPTION_NAME); - } - - /** - * Return the value for the given option name. - * - * @param name Option name - * @return Option value - */ - public String getOption(String name) { - return options != null ? options.get(name) : null; - } - - /** - * Returns a CQL query representing this index. - * - *

This method returns a single 'CREATE INDEX' query corresponding to this index definition. - * - * @return the 'CREATE INDEX' query corresponding to this index. - */ - public String asCQLQuery() { - String keyspaceName = Metadata.quoteIfNecessary(table.getKeyspace().getName()); - String tableName = Metadata.quoteIfNecessary(table.getName()); - String indexName = Metadata.quoteIfNecessary(this.name); - return isCustomIndex() - ? String.format( - "CREATE CUSTOM INDEX %s ON %s.%s (%s) USING '%s' %s;", - indexName, keyspaceName, tableName, getTarget(), getIndexClassName(), getOptionsAsCql()) - : String.format( - "CREATE INDEX %s ON %s.%s (%s);", indexName, keyspaceName, tableName, getTarget()); - } - - /** - * Builds a string representation of the custom index options. - * - * @return String representation of the custom index options, similar to what Cassandra stores in - * the 'index_options' column of the 'schema_columns' table in the 'system' keyspace. - */ - private String getOptionsAsCql() { - Iterable> filtered = - Iterables.filter( - options.entrySet(), - new Predicate>() { - @Override - public boolean apply(Map.Entry input) { - return !input.getKey().equals(TARGET_OPTION_NAME) - && !input.getKey().equals(CUSTOM_INDEX_OPTION_NAME); - } - }); - if (Iterables.isEmpty(filtered)) return ""; - StringBuilder builder = new StringBuilder(); - builder.append("WITH OPTIONS = {"); - Iterator> it = filtered.iterator(); - while (it.hasNext()) { - Map.Entry option = it.next(); - builder.append(String.format("'%s' : '%s'", option.getKey(), option.getValue())); - if (it.hasNext()) builder.append(", "); - } - builder.append("}"); - return builder.toString(); - } - - public int hashCode() { - return MoreObjects.hashCode(name, kind, target, options); - } - - public boolean equals(Object obj) { - if (obj == this) return true; - - if (!(obj instanceof IndexMetadata)) return false; - - IndexMetadata other = (IndexMetadata) obj; - - return MoreObjects.equal(name, other.name) - && MoreObjects.equal(kind, other.kind) - && MoreObjects.equal(target, other.target) - && MoreObjects.equal(options, other.options); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/JdkSSLOptions.java b/driver-core/src/main/java/com/datastax/driver/core/JdkSSLOptions.java deleted file mode 100644 index e61db79ab8b..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/JdkSSLOptions.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.ssl.SslHandler; -import java.security.NoSuchAlgorithmException; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; - -/** - * {@link SSLOptions} implementation based on built-in JDK classes. - * - * @deprecated Use {@link RemoteEndpointAwareJdkSSLOptions} instead. - */ -@SuppressWarnings("DeprecatedIsStillUsed") -@Deprecated -public class JdkSSLOptions implements SSLOptions { - - /** - * Creates a builder to create a new instance. - * - * @return the builder. - */ - public static Builder builder() { - return new Builder(); - } - - protected final SSLContext context; - protected final String[] cipherSuites; - - /** - * Creates a new instance. - * - * @param context the SSL context. - * @param cipherSuites the cipher suites to use. - */ - protected JdkSSLOptions(SSLContext context, String[] cipherSuites) { - this.context = (context == null) ? makeDefaultContext() : context; - this.cipherSuites = cipherSuites; - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel) { - SSLEngine engine = newSSLEngine(channel); - return new SslHandler(engine); - } - - /** - * Creates an SSL engine each time a connection is established. - * - *

- * - *

You might want to override this if you need to fine-tune the engine's configuration (for - * example enabling hostname verification). - * - * @param channel the Netty channel for that connection. - * @return the engine. - */ - protected SSLEngine newSSLEngine(@SuppressWarnings("unused") SocketChannel channel) { - SSLEngine engine = context.createSSLEngine(); - engine.setUseClientMode(true); - if (cipherSuites != null) engine.setEnabledCipherSuites(cipherSuites); - return engine; - } - - private static SSLContext makeDefaultContext() throws IllegalStateException { - try { - return SSLContext.getDefault(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Cannot initialize SSL Context", e); - } - } - - /** Helper class to build JDK-based SSL options. */ - public static class Builder { - protected SSLContext context; - protected String[] cipherSuites; - - /** - * Set the SSL context to use. - * - *

If this method isn't called, a context with the default options will be used, and you can - * use the default JSSE - * System properties to customize its behavior. This may in particular involve creating - * a simple keyStore and trustStore. - * - * @param context the SSL context. - * @return this builder. - */ - public Builder withSSLContext(SSLContext context) { - this.context = context; - return this; - } - - /** - * Set the cipher suites to use. - * - *

If this method isn't called, the default is to present all the eligible client ciphers to - * the server. - * - * @param cipherSuites the cipher suites to use. - * @return this builder. - */ - public Builder withCipherSuites(String[] cipherSuites) { - this.cipherSuites = cipherSuites; - return this; - } - - /** - * Builds a new instance based on the parameters provided to this builder. - * - * @return the new instance. - */ - @SuppressWarnings("deprecation") - public JdkSSLOptions build() { - return new JdkSSLOptions(context, cipherSuites); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/KeyspaceMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/KeyspaceMetadata.java deleted file mode 100644 index 2f893cf42c8..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/KeyspaceMetadata.java +++ /dev/null @@ -1,461 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSortedSet; -import com.google.common.collect.Lists; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** Describes a keyspace defined in this cluster. */ -public class KeyspaceMetadata { - - public static final String KS_NAME = "keyspace_name"; - private static final String DURABLE_WRITES = "durable_writes"; - private static final String STRATEGY_CLASS = "strategy_class"; - private static final String STRATEGY_OPTIONS = "strategy_options"; - private static final String REPLICATION = "replication"; - - private final String name; - private final boolean durableWrites; - private final boolean virtual; - - private final ReplicationStrategy strategy; - private final Map replication; - - final Map tables = new ConcurrentHashMap(); - final Map views = - new ConcurrentHashMap(); - final Map userTypes = - Collections.synchronizedMap(new LinkedHashMap()); - final Map functions = new ConcurrentHashMap(); - final Map aggregates = - new ConcurrentHashMap(); - - @VisibleForTesting - @Deprecated - KeyspaceMetadata(String name, boolean durableWrites, Map replication) { - this(name, durableWrites, replication, false); - } - - @VisibleForTesting - KeyspaceMetadata( - String name, boolean durableWrites, Map replication, boolean virtual) { - this.name = name; - this.durableWrites = durableWrites; - this.replication = replication; - this.strategy = ReplicationStrategy.create(replication); - this.virtual = virtual; - } - - static KeyspaceMetadata build(Row row, VersionNumber cassandraVersion) { - if (cassandraVersion.getMajor() <= 2) { - String name = row.getString(KS_NAME); - boolean durableWrites = row.getBool(DURABLE_WRITES); - Map replicationOptions; - replicationOptions = new HashMap(); - replicationOptions.put("class", row.getString(STRATEGY_CLASS)); - replicationOptions.putAll(SimpleJSONParser.parseStringMap(row.getString(STRATEGY_OPTIONS))); - return new KeyspaceMetadata(name, durableWrites, replicationOptions, false); - } else { - String name = row.getString(KS_NAME); - boolean durableWrites = row.getBool(DURABLE_WRITES); - return new KeyspaceMetadata( - name, durableWrites, row.getMap(REPLICATION, String.class, String.class), false); - } - } - - static KeyspaceMetadata buildVirtual(Row row, VersionNumber cassandraVersion) { - String name = row.getString(KS_NAME); - return new KeyspaceMetadata(name, false, Collections.emptyMap(), true); - } - - /** - * Returns the name of this keyspace. - * - * @return the name of this CQL keyspace. - */ - public String getName() { - return name; - } - - /** - * Returns whether durable writes are set on this keyspace. - * - * @return {@code true} if durable writes are set on this keyspace (the default), {@code false} - * otherwise. - */ - public boolean isDurableWrites() { - return durableWrites; - } - - /** - * Returns whether or not this keyspace is a virtual keyspace - * - * @return {@code true} if virtual keyspace default), {@code false} otherwise. - */ - public boolean isVirtual() { - return virtual; - } - - /** - * Returns the replication options for this keyspace. - * - * @return a map containing the replication options for this keyspace. - */ - public Map getReplication() { - return Collections.unmodifiableMap(replication); - } - - /** - * Returns the metadata for a table contained in this keyspace. - * - * @param name the name of table to retrieve - * @return the metadata for table {@code name} if it exists in this keyspace, {@code null} - * otherwise. - */ - public TableMetadata getTable(String name) { - return tables.get(Metadata.handleId(name)); - } - - TableMetadata removeTable(String table) { - return tables.remove(table); - } - - /** - * Returns the tables defined in this keyspace. - * - * @return a collection of the metadata for the tables defined in this keyspace. - */ - public Collection getTables() { - return Collections.unmodifiableCollection(tables.values()); - } - - /** - * Returns the metadata for a materialized view contained in this keyspace. - * - * @param name the name of materialized view to retrieve - * @return the metadata for materialized view {@code name} if it exists in this keyspace, {@code - * null} otherwise. - */ - public MaterializedViewMetadata getMaterializedView(String name) { - return views.get(Metadata.handleId(name)); - } - - MaterializedViewMetadata removeMaterializedView(String materializedView) { - return views.remove(materializedView); - } - - /** - * Returns the materialized views defined in this keyspace. - * - * @return a collection of the metadata for the materialized views defined in this keyspace. - */ - public Collection getMaterializedViews() { - return Collections.unmodifiableCollection(views.values()); - } - - /** - * Returns the definition for a user defined type (UDT) in this keyspace. - * - * @param name the name of UDT definition to retrieve - * @return the definition for {@code name} if it exists in this keyspace, {@code null} otherwise. - */ - public UserType getUserType(String name) { - return userTypes.get(Metadata.handleId(name)); - } - - /** - * Returns the user types defined in this keyspace. - * - * @return a collection of the definition for the user types defined in this keyspace. - */ - public Collection getUserTypes() { - return Collections.unmodifiableCollection(userTypes.values()); - } - - UserType removeUserType(String userType) { - return userTypes.remove(userType); - } - - /** - * Returns the definition of a function in this keyspace. - * - * @param name the name of the function. - * @param argumentTypes the types of the function's arguments. - * @return the function definition if it exists in this keyspace, {@code null} otherwise. - */ - public FunctionMetadata getFunction(String name, Collection argumentTypes) { - return functions.get(Metadata.fullFunctionName(Metadata.handleId(name), argumentTypes)); - } - - /** - * Returns the definition of a function in this keyspace. - * - * @param name the name of the function. - * @param argumentTypes the types of the function's arguments. - * @return the function definition if it exists in this keyspace, {@code null} otherwise. - */ - public FunctionMetadata getFunction(String name, DataType... argumentTypes) { - return getFunction(name, Lists.newArrayList(argumentTypes)); - } - - /** - * Returns the functions defined in this keyspace. - * - * @return a collection of the definition for the functions defined in this keyspace. - */ - public Collection getFunctions() { - return Collections.unmodifiableCollection(functions.values()); - } - - FunctionMetadata removeFunction(String fullName) { - return functions.remove(fullName); - } - - /** - * Returns the definition of an aggregate in this keyspace. - * - * @param name the name of the aggregate. - * @param argumentTypes the types of the aggregate's arguments. - * @return the aggregate definition if it exists in this keyspace, {@code null} otherwise. - */ - public AggregateMetadata getAggregate(String name, Collection argumentTypes) { - return aggregates.get(Metadata.fullFunctionName(Metadata.handleId(name), argumentTypes)); - } - - /** - * Returns the definition of an aggregate in this keyspace. - * - * @param name the name of the aggregate. - * @param argumentTypes the types of the aggregate's arguments. - * @return the aggregate definition if it exists in this keyspace, {@code null} otherwise. - */ - public AggregateMetadata getAggregate(String name, DataType... argumentTypes) { - return getAggregate(name, Lists.newArrayList(argumentTypes)); - } - - /** - * Returns the aggregates defined in this keyspace. - * - * @return a collection of the definition for the aggregates defined in this keyspace. - */ - public Collection getAggregates() { - return Collections.unmodifiableCollection(aggregates.values()); - } - - AggregateMetadata removeAggregate(String fullName) { - return aggregates.remove(fullName); - } - - // comparators for ordering types in cqlsh output. - - private static final Comparator typeByName = - new Comparator() { - @Override - public int compare(UserType o1, UserType o2) { - return o1.getTypeName().compareTo(o2.getTypeName()); - } - }; - - private static final Comparator functionByName = - new Comparator() { - @Override - public int compare(FunctionMetadata o1, FunctionMetadata o2) { - return o1.getSimpleName().compareTo(o2.getSimpleName()); - } - }; - - private static final Comparator aggregateByName = - new Comparator() { - @Override - public int compare(AggregateMetadata o1, AggregateMetadata o2) { - return o1.getSimpleName().compareTo(o2.getSimpleName()); - } - }; - - /** - * Returns a {@code String} containing CQL queries representing this keyspace and the user types - * and tables it contains. - * - *

In other words, this method returns the queries that would allow to recreate the schema of - * this keyspace, along with all its user types/tables. - * - *

Note that the returned String is formatted to be human readable (for some definition of - * human readable at least). - * - * @return the CQL queries representing this keyspace schema as a {code String}. - */ - public String exportAsString() { - StringBuilder sb = new StringBuilder(); - - sb.append(asCQLQuery()).append('\n'); - - // include types, tables, views, functions and aggregates, each ordered by name, with one small - // exception - // being that user types are ordered topologically and then by name within same level. - for (UserType udt : getSortedUserTypes()) - sb.append('\n').append(udt.exportAsString()).append('\n'); - - for (AbstractTableMetadata tm : - ImmutableSortedSet.orderedBy(AbstractTableMetadata.byNameComparator) - .addAll(tables.values()) - .build()) sb.append('\n').append(tm.exportAsString()).append('\n'); - - for (FunctionMetadata fm : - ImmutableSortedSet.orderedBy(functionByName).addAll(functions.values()).build()) - sb.append('\n').append(fm.exportAsString()).append('\n'); - - for (AggregateMetadata am : - ImmutableSortedSet.orderedBy(aggregateByName).addAll(aggregates.values()).build()) - sb.append('\n').append(am.exportAsString()).append('\n'); - - return sb.toString(); - } - - private List getSortedUserTypes() { - // rebuilds dependency tree of user types so they may be sorted within each dependency level. - List unsortedTypes = new ArrayList(userTypes.values()); - DirectedGraph graph = new DirectedGraph(typeByName, unsortedTypes); - for (UserType from : unsortedTypes) { - for (UserType to : unsortedTypes) { - if (from != to && dependsOn(to, from)) graph.addEdge(from, to); - } - } - return graph.topologicalSort(); - } - - private boolean dependsOn(UserType udt1, UserType udt2) { - for (UserType.Field field : udt1) { - if (references(field.getType(), udt2)) { - return true; - } - } - return false; - } - - private boolean references(DataType dataType, DataType udtType) { - if (dataType.equals(udtType)) return true; - for (DataType arg : dataType.getTypeArguments()) { - if (references(arg, udtType)) return true; - } - if (dataType instanceof TupleType) { - for (DataType arg : ((TupleType) dataType).getComponentTypes()) { - if (references(arg, udtType)) return true; - } - } - return false; - } - - /** - * Returns a CQL query representing this keyspace. - * - *

This method returns a single 'CREATE KEYSPACE' query with the options corresponding to this - * keyspace definition. - * - * @return the 'CREATE KEYSPACE' query corresponding to this keyspace. - * @see #exportAsString - */ - public String asCQLQuery() { - StringBuilder sb = new StringBuilder(); - if (virtual) { - sb.append("/* VIRTUAL "); - } else { - sb.append("CREATE "); - } - - sb.append("KEYSPACE ").append(Metadata.quoteIfNecessary(name)).append(" WITH "); - sb.append("REPLICATION = { 'class' : '").append(replication.get("class")).append('\''); - for (Map.Entry entry : replication.entrySet()) { - if (entry.getKey().equals("class")) continue; - sb.append(", '").append(entry.getKey()).append("': '").append(entry.getValue()).append('\''); - } - sb.append(" } AND DURABLE_WRITES = ").append(durableWrites); - sb.append(';'); - if (virtual) { - sb.append("*/"); - } - return sb.toString(); - } - - @Override - public String toString() { - if (virtual) { - return name; - } - return asCQLQuery(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - KeyspaceMetadata that = (KeyspaceMetadata) o; - - if (durableWrites != that.durableWrites) return false; - if (!name.equals(that.name)) return false; - if (strategy != null ? !strategy.equals(that.strategy) : that.strategy != null) return false; - if (!replication.equals(that.replication)) return false; - return tables.equals(that.tables); - } - - @Override - public int hashCode() { - int result = name.hashCode(); - result = 31 * result + (durableWrites ? 1 : 0); - result = 31 * result + (strategy != null ? strategy.hashCode() : 0); - result = 31 * result + replication.hashCode(); - result = 31 * result + tables.hashCode(); - return result; - } - - void add(TableMetadata tm) { - tables.put(tm.getName(), tm); - } - - void add(MaterializedViewMetadata view) { - views.put(view.getName(), view); - } - - void add(FunctionMetadata function) { - String functionName = - Metadata.fullFunctionName(function.getSimpleName(), function.getArguments().values()); - functions.put(functionName, function); - } - - void add(AggregateMetadata aggregate) { - String aggregateName = - Metadata.fullFunctionName(aggregate.getSimpleName(), aggregate.getArgumentTypes()); - aggregates.put(aggregateName, aggregate); - } - - void add(UserType type) { - userTypes.put(type.getTypeName(), type); - } - - ReplicationStrategy replicationStrategy() { - return strategy; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/LZ4Compressor.java b/driver-core/src/main/java/com/datastax/driver/core/LZ4Compressor.java deleted file mode 100644 index a20f400be8e..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/LZ4Compressor.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.buffer.ByteBuf; -import java.io.IOException; -import java.nio.ByteBuffer; -import net.jpountz.lz4.LZ4Factory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -class LZ4Compressor extends FrameCompressor { - - private static final Logger logger = LoggerFactory.getLogger(LZ4Compressor.class); - - static final LZ4Compressor instance; - - static { - LZ4Compressor i; - try { - i = new LZ4Compressor(); - } catch (NoClassDefFoundError e) { - i = null; - logger.warn( - "Cannot find LZ4 class, you should make sure the LZ4 library is in the classpath if you intend to use it. LZ4 compression will not be available for the protocol."); - } catch (Throwable e) { - i = null; - logger.warn( - "Error loading LZ4 library ({}). LZ4 compression will not be available for the protocol.", - e.toString()); - } - instance = i; - } - - private static final int INTEGER_BYTES = 4; - private final net.jpountz.lz4.LZ4Compressor compressor; - private final net.jpountz.lz4.LZ4FastDecompressor decompressor; - - private LZ4Compressor() { - final LZ4Factory lz4Factory = LZ4Factory.fastestInstance(); - logger.info("Using {}", lz4Factory.toString()); - compressor = lz4Factory.fastCompressor(); - decompressor = lz4Factory.fastDecompressor(); - } - - @Override - Frame compress(Frame frame) throws IOException { - ByteBuf input = frame.body; - ByteBuf frameBody = compress(input, true); - return frame.with(frameBody); - } - - @Override - ByteBuf compress(ByteBuf buffer) throws IOException { - return compress(buffer, false); - } - - private ByteBuf compress(ByteBuf buffer, boolean prependWithUncompressedLength) - throws IOException { - return buffer.isDirect() - ? compressDirect(buffer, prependWithUncompressedLength) - : compressHeap(buffer, prependWithUncompressedLength); - } - - private ByteBuf compressDirect(ByteBuf input, boolean prependWithUncompressedLength) - throws IOException { - int maxCompressedLength = compressor.maxCompressedLength(input.readableBytes()); - // If the input is direct we will allocate a direct output buffer as well as this will allow us - // to use - // LZ4Compressor.compress and so eliminate memory copies. - ByteBuf output = - input - .alloc() - .directBuffer( - (prependWithUncompressedLength ? INTEGER_BYTES : 0) + maxCompressedLength); - try { - ByteBuffer in = inputNioBuffer(input); - // Increase reader index. - input.readerIndex(input.writerIndex()); - - if (prependWithUncompressedLength) { - output.writeInt(in.remaining()); - } - - ByteBuffer out = outputNioBuffer(output); - int written = - compressor.compress( - in, in.position(), in.remaining(), out, out.position(), out.remaining()); - // Set the writer index so the amount of written bytes is reflected - output.writerIndex(output.writerIndex() + written); - } catch (Exception e) { - // release output buffer so we not leak and rethrow exception. - output.release(); - throw new IOException(e); - } - return output; - } - - private ByteBuf compressHeap(ByteBuf input, boolean prependWithUncompressedLength) - throws IOException { - int maxCompressedLength = compressor.maxCompressedLength(input.readableBytes()); - - // Not a direct buffer so use byte arrays... - int inOffset = input.arrayOffset() + input.readerIndex(); - byte[] in = input.array(); - int len = input.readableBytes(); - // Increase reader index. - input.readerIndex(input.writerIndex()); - - // Allocate a heap buffer from the ByteBufAllocator as we may use a PooledByteBufAllocator and - // so - // can eliminate the overhead of allocate a new byte[]. - ByteBuf output = - input - .alloc() - .heapBuffer((prependWithUncompressedLength ? INTEGER_BYTES : 0) + maxCompressedLength); - try { - if (prependWithUncompressedLength) { - output.writeInt(len); - } - // calculate the correct offset. - int offset = output.arrayOffset() + output.writerIndex(); - byte[] out = output.array(); - int written = compressor.compress(in, inOffset, len, out, offset); - - // Set the writer index so the amount of written bytes is reflected - output.writerIndex(output.writerIndex() + written); - } catch (Exception e) { - // release output buffer so we not leak and rethrow exception. - output.release(); - throw new IOException(e); - } - return output; - } - - @Override - Frame decompress(Frame frame) throws IOException { - ByteBuf input = frame.body; - int uncompressedLength = input.readInt(); - ByteBuf frameBody = decompress(input, uncompressedLength); - return frame.with(frameBody); - } - - @Override - ByteBuf decompress(ByteBuf buffer, int uncompressedLength) throws IOException { - return buffer.isDirect() - ? decompressDirect(buffer, uncompressedLength) - : decompressHeap(buffer, uncompressedLength); - } - - private ByteBuf decompressDirect(ByteBuf input, int uncompressedLength) throws IOException { - // If the input is direct we will allocate a direct output buffer as well as this will allow us - // to use - // LZ4Compressor.decompress and so eliminate memory copies. - int readable = input.readableBytes(); - ByteBuffer in = inputNioBuffer(input); - // Increase reader index. - input.readerIndex(input.writerIndex()); - ByteBuf output = input.alloc().directBuffer(uncompressedLength); - try { - ByteBuffer out = outputNioBuffer(output); - int read = decompressor.decompress(in, in.position(), out, out.position(), out.remaining()); - if (read != readable) throw new IOException("Compressed lengths mismatch"); - - // Set the writer index so the amount of written bytes is reflected - output.writerIndex(output.writerIndex() + uncompressedLength); - } catch (Exception e) { - // release output buffer so we not leak and rethrow exception. - output.release(); - throw new IOException(e); - } - return output; - } - - private ByteBuf decompressHeap(ByteBuf input, int uncompressedLength) throws IOException { - // Not a direct buffer so use byte arrays... - byte[] in = input.array(); - int len = input.readableBytes(); - int inOffset = input.arrayOffset() + input.readerIndex(); - // Increase reader index. - input.readerIndex(input.writerIndex()); - - // Allocate a heap buffer from the ByteBufAllocator as we may use a PooledByteBufAllocator and - // so - // can eliminate the overhead of allocate a new byte[]. - ByteBuf output = input.alloc().heapBuffer(uncompressedLength); - try { - int offset = output.arrayOffset() + output.writerIndex(); - byte out[] = output.array(); - int read = decompressor.decompress(in, inOffset, out, offset, uncompressedLength); - if (read != len) throw new IOException("Compressed lengths mismatch"); - - // Set the writer index so the amount of written bytes is reflected - output.writerIndex(output.writerIndex() + uncompressedLength); - } catch (Exception e) { - // release output buffer so we not leak and rethrow exception. - output.release(); - throw new IOException(e); - } - return output; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/LatencyTracker.java b/driver-core/src/main/java/com/datastax/driver/core/LatencyTracker.java deleted file mode 100644 index 27a248629cf..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/LatencyTracker.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Interface for objects that are interested in tracking the latencies of the driver queries to each - * Cassandra nodes. - * - *

An implementation of this interface can be registered against a Cluster object trough the - * {@link Cluster#register} method, after which the {@link #update(Host, Statement, Exception, - * long)} method will be called after each query of the driver to a Cassandra host with the - * latency/duration (in nanoseconds) of this operation. - */ -public interface LatencyTracker { - - /** - * A method that is called after each request to a Cassandra node with the duration of that - * operation. - * - *

Note that there is no guarantee that this method won't be called concurrently by multiple - * threads, so implementations should synchronize internally if need be. - * - * @param host The Cassandra host on which a request has been performed. This parameter is never - * {@code null}. - * @param statement The {@link com.datastax.driver.core.Statement} that has been executed. This - * parameter is never {@code null}. - * @param exception An {@link Exception} thrown when receiving the response, or {@code null} if - * the response was successful. - * @param newLatencyNanos the latency in nanoseconds of the operation. This latency corresponds to - * the time elapsed between when the query was sent to {@code host} and when the response was - * received by the driver (or the operation timed out, in which {@code newLatencyNanos} will - * approximately be the timeout value). - */ - public void update(Host host, Statement statement, Exception exception, long newLatencyNanos); - - /** - * Gets invoked when the tracker is registered with a cluster, or at cluster startup if the - * tracker was registered at initialization with {@link - * com.datastax.driver.core.Cluster.Initializer#register(LatencyTracker)}. - * - * @param cluster the cluster that this tracker is registered with. - */ - void onRegister(Cluster cluster); - - /** - * Gets invoked when the tracker is unregistered from a cluster, or at cluster shutdown if the - * tracker was not unregistered. - * - * @param cluster the cluster that this tracker was registered with. - */ - void onUnregister(Cluster cluster); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/LocalDate.java b/driver-core/src/main/java/com/datastax/driver/core/LocalDate.java deleted file mode 100644 index 8b6bb33a317..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/LocalDate.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.google.common.base.Preconditions.checkArgument; - -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.TimeZone; -import java.util.concurrent.TimeUnit; - -/** - * A date with no time components, no time zone, in the ISO 8601 calendar. - * - *

Note that ISO 8601 has a number of differences with the default gregorian calendar used in - * Java: - * - *

    - *
  • it uses a proleptic gregorian calendar, meaning that it's gregorian indefinitely back in - * the past (there is no gregorian change); - *
  • there is a year 0. - *
- * - *

This class implements these differences, so that year/month/day fields match exactly the ones - * in CQL string literals. - * - * @since 2.2 - */ -public final class LocalDate { - - private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); - - private final long millisSinceEpoch; - private final int daysSinceEpoch; - - // This gets initialized lazily if we ever need it. Once set, it is effectively immutable. - private volatile GregorianCalendar calendar; - - private LocalDate(int daysSinceEpoch) { - this.daysSinceEpoch = daysSinceEpoch; - this.millisSinceEpoch = TimeUnit.DAYS.toMillis(daysSinceEpoch); - } - - /** - * Builds a new instance from a number of days since January 1st, 1970 GMT. - * - * @param daysSinceEpoch the number of days. - * @return the new instance. - */ - public static LocalDate fromDaysSinceEpoch(int daysSinceEpoch) { - return new LocalDate(daysSinceEpoch); - } - - /** - * Builds a new instance from a number of milliseconds since January 1st, 1970 GMT. Note that if - * the given number does not correspond to a whole number of days, it will be rounded towards 0. - * - * @param millisSinceEpoch the number of milliseconds since January 1st, 1970 GMT. - * @return the new instance. - * @throws IllegalArgumentException if the date is not in the range [-5877641-06-23; - * 5881580-07-11]. - */ - public static LocalDate fromMillisSinceEpoch(long millisSinceEpoch) - throws IllegalArgumentException { - long daysSinceEpoch = TimeUnit.MILLISECONDS.toDays(millisSinceEpoch); - checkArgument( - daysSinceEpoch >= Integer.MIN_VALUE && daysSinceEpoch <= Integer.MAX_VALUE, - "Date should be in the range [-5877641-06-23; 5881580-07-11]"); - - return new LocalDate((int) daysSinceEpoch); - } - - /** - * Builds a new instance from a year/month/day specification. - * - *

This method is not lenient, i.e. '2014-12-32' will not be treated as '2015-01-01', but - * instead throw an {@code IllegalArgumentException}. - * - * @param year the year in ISO format (see {@link LocalDate this class's Javadoc}). - * @param month the month. It is 1-based (e.g. 1 for January). - * @param dayOfMonth the day of the month. - * @return the new instance. - * @throws IllegalArgumentException if the corresponding date does not exist in the ISO8601 - * calendar. - */ - public static LocalDate fromYearMonthDay(int year, int month, int dayOfMonth) { - int calendarYear = (year <= 0) ? -year + 1 : year; - int calendarEra = (year <= 0) ? GregorianCalendar.BC : GregorianCalendar.AD; - - GregorianCalendar calendar = isoCalendar(); - // We can't allow leniency because that could mess with our year shift above (for example if the - // arguments were 0, 12, 32) - calendar.setLenient(false); - calendar.clear(); - calendar.set(calendarYear, month - 1, dayOfMonth, 0, 0, 0); - calendar.set(Calendar.ERA, calendarEra); - - LocalDate date = fromMillisSinceEpoch(calendar.getTimeInMillis()); - date.calendar = calendar; - return date; - } - - /** - * Returns the number of days since January 1st, 1970 GMT. - * - * @return the number of days. - */ - public int getDaysSinceEpoch() { - return daysSinceEpoch; - } - - /** - * Returns the number of milliseconds since January 1st, 1970 GMT. - * - * @return the number of milliseconds. - */ - public long getMillisSinceEpoch() { - return millisSinceEpoch; - } - - /** - * Returns the year. - * - * @return the year. - */ - public int getYear() { - GregorianCalendar c = getCalendar(); - int year = c.get(Calendar.YEAR); - if (c.get(Calendar.ERA) == GregorianCalendar.BC) year = -year + 1; - return year; - } - - /** - * Returns the month. - * - * @return the month. It is 1-based, e.g. 1 for January. - */ - public int getMonth() { - return getCalendar().get(Calendar.MONTH) + 1; - } - - /** - * Returns the day in the month. - * - * @return the day in the month. - */ - public int getDay() { - return getCalendar().get(Calendar.DAY_OF_MONTH); - } - - /** - * Return a new {@link LocalDate} with the specified (signed) amount of time added to (or - * subtracted from) the given {@link Calendar} field, based on the calendar's rules. - * - *

Note that adding any amount to a field smaller than {@link Calendar#DAY_OF_MONTH} will - * remain without effect, as this class does not keep time components. - * - *

See {@link Calendar} javadocs for more information. - * - * @param field a {@link Calendar} field to modify. - * @param amount the amount of date or time to be added to the field. - * @return a new {@link LocalDate} with the specified (signed) amount of time added to (or - * subtracted from) the given {@link Calendar} field. - * @throws IllegalArgumentException if the new date is not in the range [-5877641-06-23; - * 5881580-07-11]. - */ - public LocalDate add(int field, int amount) { - GregorianCalendar newCalendar = isoCalendar(); - newCalendar.setTimeInMillis(millisSinceEpoch); - newCalendar.add(field, amount); - LocalDate newDate = fromMillisSinceEpoch(newCalendar.getTimeInMillis()); - newDate.calendar = newCalendar; - return newDate; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - - if (o instanceof LocalDate) { - LocalDate that = (LocalDate) o; - return this.daysSinceEpoch == that.daysSinceEpoch; - } - return false; - } - - @Override - public int hashCode() { - return daysSinceEpoch; - } - - @Override - public String toString() { - return String.format("%d-%s-%s", getYear(), pad2(getMonth()), pad2(getDay())); - } - - private static String pad2(int i) { - String s = Integer.toString(i); - return s.length() == 2 ? s : "0" + s; - } - - private GregorianCalendar getCalendar() { - // Two threads can race and both create a calendar. This is not a problem. - if (calendar == null) { - - // Use a local variable to only expose after we're done mutating it. - GregorianCalendar tmp = isoCalendar(); - tmp.setTimeInMillis(millisSinceEpoch); - - calendar = tmp; - } - return calendar; - } - - // This matches what Cassandra uses server side (from Joda Time's LocalDate) - private static GregorianCalendar isoCalendar() { - GregorianCalendar calendar = new GregorianCalendar(UTC); - calendar.setGregorianChange(new Date(Long.MIN_VALUE)); - return calendar; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/LoggingMonotonicTimestampGenerator.java b/driver-core/src/main/java/com/datastax/driver/core/LoggingMonotonicTimestampGenerator.java deleted file mode 100644 index 1667af3fc84..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/LoggingMonotonicTimestampGenerator.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static java.util.concurrent.TimeUnit.MICROSECONDS; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A monotonic timestamp generator that logs warnings when timestamps drift in the future (see this - * class's constructors and {@link #onDrift(long, long)} for more information). - */ -public abstract class LoggingMonotonicTimestampGenerator - extends AbstractMonotonicTimestampGenerator { - private static final Logger LOGGER = LoggerFactory.getLogger(TimestampGenerator.class); - - private final long warningThresholdMicros; - private final long warningIntervalMillis; - - private final AtomicLong lastDriftWarning = new AtomicLong(Long.MIN_VALUE); - - /** - * Creates a new instance. - * - * @param warningThreshold how far in the future timestamps are allowed to drift before a warning - * is logged. - * @param warningThresholdUnit the unit for {@code warningThreshold}. - * @param warningInterval how often the warning will be logged if timestamps keep drifting above - * the threshold. - * @param warningIntervalUnit the unit for {@code warningIntervalUnit}. - */ - protected LoggingMonotonicTimestampGenerator( - long warningThreshold, - TimeUnit warningThresholdUnit, - long warningInterval, - TimeUnit warningIntervalUnit) { - this.warningThresholdMicros = MICROSECONDS.convert(warningThreshold, warningThresholdUnit); - this.warningIntervalMillis = MILLISECONDS.convert(warningInterval, warningIntervalUnit); - } - - /** - * {@inheritDoc} - * - *

This implementation logs a warning at regular intervals when timestamps drift more than a - * specified threshold in the future. These messages are emitted at {@code WARN} level in the - * category {@code com.datastax.driver.core.TimestampGenerator}. - * - * @param currentTick the current clock tick. - * @param lastTimestamp the last timestamp that was generated. - */ - protected void onDrift(long currentTick, long lastTimestamp) { - if (LOGGER.isWarnEnabled() - && warningThresholdMicros >= 0 - && lastTimestamp > currentTick + warningThresholdMicros) { - long now = System.currentTimeMillis(); - long lastWarning = lastDriftWarning.get(); - if (now > lastWarning + warningIntervalMillis - && lastDriftWarning.compareAndSet(lastWarning, now)) { - LOGGER.warn( - "Clock skew detected: current tick ({}) was {} microseconds behind the last generated timestamp ({}), " - + "returned timestamps will be artificially incremented to guarantee monotonicity.", - currentTick, - lastTimestamp - currentTick, - lastTimestamp); - } - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/MD5Digest.java b/driver-core/src/main/java/com/datastax/driver/core/MD5Digest.java deleted file mode 100644 index 1edef4421a9..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/MD5Digest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.Bytes; -import java.util.Arrays; - -/** - * The result of the computation of an MD5 digest. - * - *

A MD5 is really just a byte[] but arrays are a no go as map keys. We could wrap it in a - * ByteBuffer but: 1. MD5Digest is a more explicit name than ByteBuffer to represent a md5. 2. Using - * our own class allows to use our FastByteComparison for equals. - */ -class MD5Digest { - - public final byte[] bytes; - - private MD5Digest(byte[] bytes) { - this.bytes = bytes; - } - - public static MD5Digest wrap(byte[] digest) { - return new MD5Digest(digest); - } - - @Override - public final int hashCode() { - return Arrays.hashCode(bytes); - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof MD5Digest)) return false; - MD5Digest that = (MD5Digest) o; - // handles nulls properly - return Arrays.equals(this.bytes, that.bytes); - } - - @Override - public String toString() { - return Bytes.toHexString(bytes); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/MaterializedViewMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/MaterializedViewMetadata.java deleted file mode 100644 index aa948d34a5a..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/MaterializedViewMetadata.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.MoreObjects; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.UUID; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * An immutable representation of a materialized view. Materialized views are available starting - * from Cassandra 3.0. - */ -public class MaterializedViewMetadata extends AbstractTableMetadata { - - private static final Logger logger = LoggerFactory.getLogger(MaterializedViewMetadata.class); - - private volatile TableMetadata baseTable; - - private final boolean includeAllColumns; - - private final String whereClause; - - private MaterializedViewMetadata( - KeyspaceMetadata keyspace, - TableMetadata baseTable, - String name, - UUID id, - List partitionKey, - List clusteringColumns, - Map columns, - boolean includeAllColumns, - String whereClause, - TableOptionsMetadata options, - List clusteringOrder, - VersionNumber cassandraVersion) { - super( - keyspace, - name, - id, - partitionKey, - clusteringColumns, - columns, - options, - clusteringOrder, - cassandraVersion); - this.baseTable = baseTable; - this.includeAllColumns = includeAllColumns; - this.whereClause = whereClause; - } - - static MaterializedViewMetadata build( - KeyspaceMetadata keyspace, - Row row, - Map rawCols, - VersionNumber cassandraVersion, - Cluster cluster) { - - String name = row.getString("view_name"); - String tableName = row.getString("base_table_name"); - TableMetadata baseTable = keyspace.tables.get(tableName); - if (baseTable == null) { - logger.trace( - String.format( - "Cannot find base table %s for materialized view %s.%s: " - + "Cluster.getMetadata().getKeyspace(\"%s\").getView(\"%s\") will return null", - tableName, keyspace.getName(), name, keyspace.getName(), name)); - return null; - } - - UUID id = row.getUUID("id"); - boolean includeAllColumns = row.getBool("include_all_columns"); - String whereClause = row.getString("where_clause"); - - int partitionKeySize = - findCollectionSize(rawCols.values(), ColumnMetadata.Raw.Kind.PARTITION_KEY); - int clusteringSize = - findCollectionSize(rawCols.values(), ColumnMetadata.Raw.Kind.CLUSTERING_COLUMN); - - List partitionKey = - new ArrayList(Collections.nCopies(partitionKeySize, null)); - List clusteringColumns = - new ArrayList(Collections.nCopies(clusteringSize, null)); - List clusteringOrder = - new ArrayList(Collections.nCopies(clusteringSize, null)); - - // We use a linked hashmap because we will keep this in the order of a 'SELECT * FROM ...'. - LinkedHashMap columns = new LinkedHashMap(); - - TableOptionsMetadata options = null; - try { - options = new TableOptionsMetadata(row, false, cassandraVersion); - } catch (RuntimeException e) { - // See ControlConnection#refreshSchema for why we'd rather not probably this further. Since - // table options is one thing - // that tends to change often in Cassandra, it's worth special casing this. - logger.error( - String.format( - "Error parsing schema options for view %s.%s: " - + "Cluster.getMetadata().getKeyspace(\"%s\").getView(\"%s\").getOptions() will return null", - keyspace.getName(), name, keyspace.getName(), name), - e); - } - - MaterializedViewMetadata view = - new MaterializedViewMetadata( - keyspace, - baseTable, - name, - id, - partitionKey, - clusteringColumns, - columns, - includeAllColumns, - whereClause, - options, - clusteringOrder, - cassandraVersion); - - // We use this temporary set just so non PK columns are added in lexicographical order, which is - // the one of a - // 'SELECT * FROM ...' - Set otherColumns = new TreeSet(columnMetadataComparator); - for (ColumnMetadata.Raw rawCol : rawCols.values()) { - DataType dataType; - if (cassandraVersion.getMajor() >= 3) { - dataType = - DataTypeCqlNameParser.parse( - rawCol.dataType, - cluster, - keyspace.getName(), - keyspace.userTypes, - keyspace.userTypes, - false, - false); - } else { - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - dataType = - DataTypeClassNameParser.parseOne(rawCol.dataType, protocolVersion, codecRegistry); - } - ColumnMetadata col = ColumnMetadata.fromRaw(view, rawCol, dataType); - switch (rawCol.kind) { - case PARTITION_KEY: - partitionKey.set(rawCol.position, col); - break; - case CLUSTERING_COLUMN: - clusteringColumns.set(rawCol.position, col); - clusteringOrder.set( - rawCol.position, rawCol.isReversed ? ClusteringOrder.DESC : ClusteringOrder.ASC); - break; - default: - otherColumns.add(col); - break; - } - } - for (ColumnMetadata c : partitionKey) columns.put(c.getName(), c); - for (ColumnMetadata c : clusteringColumns) columns.put(c.getName(), c); - for (ColumnMetadata c : otherColumns) columns.put(c.getName(), c); - - baseTable.add(view); - - return view; - } - - private static int findCollectionSize( - Collection cols, ColumnMetadata.Raw.Kind kind) { - int maxId = -1; - for (ColumnMetadata.Raw col : cols) if (col.kind == kind) maxId = Math.max(maxId, col.position); - return maxId + 1; - } - - /** - * Return this materialized view's base table. - * - * @return this materialized view's base table. - */ - public TableMetadata getBaseTable() { - return baseTable; - } - - @Override - protected String asCQLQuery(boolean formatted) { - - String keyspaceName = Metadata.quoteIfNecessary(keyspace.getName()); - String baseTableName = Metadata.quoteIfNecessary(baseTable.getName()); - String viewName = Metadata.quoteIfNecessary(name); - - StringBuilder sb = new StringBuilder(); - sb.append("CREATE MATERIALIZED VIEW ") - .append(keyspaceName) - .append('.') - .append(viewName) - .append(" AS"); - - // SELECT - spaceOrNewLine(sb, formatted).append("SELECT "); - if (includeAllColumns) { - sb.append("*"); - } else { - Iterator it = columns.values().iterator(); - while (it.hasNext()) { - ColumnMetadata column = it.next(); - sb.append(Metadata.quoteIfNecessary(column.getName())); - if (it.hasNext()) sb.append(", "); - } - } - - // FROM - spaceOrNewLine(sb, formatted) - .append("FROM ") - .append(keyspaceName) - .append('.') - .append(baseTableName); - - // WHERE - // the CQL grammar allows missing WHERE clauses, although C* currently disallows it - if (whereClause != null && !whereClause.isEmpty()) { - spaceOrNewLine(sb, formatted).append("WHERE ").append(whereClause); - } - - // PK - spaceOrNewLine(sb, formatted).append("PRIMARY KEY ("); - if (partitionKey.size() == 1) { - sb.append(Metadata.quoteIfNecessary(partitionKey.get(0).getName())); - } else { - sb.append('('); - boolean first = true; - for (ColumnMetadata cm : partitionKey) { - if (first) first = false; - else sb.append(", "); - sb.append(Metadata.quoteIfNecessary(cm.getName())); - } - sb.append(')'); - } - for (ColumnMetadata cm : clusteringColumns) - sb.append(", ").append(Metadata.quoteIfNecessary(cm.getName())); - sb.append(')'); - - // append 3 extra spaces if formatted to align WITH. - spaceOrNewLine(sb, formatted); - appendOptions(sb, formatted); - return sb.toString(); - } - - /** - * Updates the base table for this view and adds it to that table. This is used when a table - * update is processed and the views need to be carried over. - */ - void setBaseTable(TableMetadata table) { - this.baseTable = table; - table.add(this); - } - - @Override - public boolean equals(Object other) { - if (other == this) return true; - if (!(other instanceof MaterializedViewMetadata)) return false; - - MaterializedViewMetadata that = (MaterializedViewMetadata) other; - return MoreObjects.equal(this.name, that.name) - && MoreObjects.equal(this.id, that.id) - && MoreObjects.equal(this.partitionKey, that.partitionKey) - && MoreObjects.equal(this.clusteringColumns, that.clusteringColumns) - && MoreObjects.equal(this.columns, that.columns) - && MoreObjects.equal(this.options, that.options) - && MoreObjects.equal(this.clusteringOrder, that.clusteringOrder) - && MoreObjects.equal(this.baseTable.getName(), that.baseTable.getName()) - && this.includeAllColumns == that.includeAllColumns; - } - - @Override - public int hashCode() { - return MoreObjects.hashCode( - name, - id, - partitionKey, - clusteringColumns, - columns, - options, - clusteringOrder, - baseTable.getName(), - includeAllColumns); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Message.java b/driver-core/src/main/java/com/datastax/driver/core/Message.java deleted file mode 100644 index 84c214dd22d..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Message.java +++ /dev/null @@ -1,416 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.exceptions.UnsupportedFeatureException; -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToMessageDecoder; -import io.netty.handler.codec.MessageToMessageEncoder; -import io.netty.util.AttributeKey; -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** A message from the CQL binary protocol. */ -abstract class Message { - - protected static final Logger logger = LoggerFactory.getLogger(Message.class); - - static AttributeKey CODEC_REGISTRY_ATTRIBUTE_KEY = - AttributeKey.valueOf("com.datastax.driver.core.CodecRegistry"); - - interface Coder { - void encode(R request, ByteBuf dest, ProtocolVersion version); - - int encodedSize(R request, ProtocolVersion version); - } - - interface Decoder { - R decode(ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry); - } - - private volatile int streamId = -1; - - /** - * A generic key-value custom payload. Custom payloads are simply ignored by the default - * QueryHandler implementation server-side. - * - * @since Protocol V4 - */ - private volatile Map customPayload; - - protected Message() {} - - Message setStreamId(int streamId) { - this.streamId = streamId; - return this; - } - - int getStreamId() { - return streamId; - } - - Map getCustomPayload() { - return customPayload; - } - - Message setCustomPayload(Map customPayload) { - this.customPayload = customPayload; - return this; - } - - abstract static class Request extends Message { - - enum Type { - STARTUP(1, Requests.Startup.coder), - CREDENTIALS(4, Requests.Credentials.coder), - OPTIONS(5, Requests.Options.coder), - QUERY(7, Requests.Query.coder), - PREPARE(9, Requests.Prepare.coder), - EXECUTE(10, Requests.Execute.coder), - REGISTER(11, Requests.Register.coder), - BATCH(13, Requests.Batch.coder), - AUTH_RESPONSE(15, Requests.AuthResponse.coder); - - final int opcode; - final Coder coder; - - Type(int opcode, Coder coder) { - this.opcode = opcode; - this.coder = coder; - } - } - - final Type type; - private final boolean tracingRequested; - - protected Request(Type type) { - this(type, false); - } - - protected Request(Type type, boolean tracingRequested) { - this.type = type; - this.tracingRequested = tracingRequested; - } - - @Override - Request setStreamId(int streamId) { - // JAVA-1179: defensively guard against reusing the same Request object twice. - // If no streamId was ever set we can use this object directly, otherwise make a copy. - if (getStreamId() < 0) return (Request) super.setStreamId(streamId); - else { - Request copy = this.copy(); - copy.setStreamId(streamId); - return copy; - } - } - - boolean isTracingRequested() { - return tracingRequested; - } - - ConsistencyLevel consistency() { - switch (this.type) { - case QUERY: - return ((Requests.Query) this).options.consistency; - case EXECUTE: - return ((Requests.Execute) this).options.consistency; - case BATCH: - return ((Requests.Batch) this).options.consistency; - default: - return null; - } - } - - ConsistencyLevel serialConsistency() { - switch (this.type) { - case QUERY: - return ((Requests.Query) this).options.serialConsistency; - case EXECUTE: - return ((Requests.Execute) this).options.serialConsistency; - case BATCH: - return ((Requests.Batch) this).options.serialConsistency; - default: - return null; - } - } - - long defaultTimestamp() { - switch (this.type) { - case QUERY: - return ((Requests.Query) this).options.defaultTimestamp; - case EXECUTE: - return ((Requests.Execute) this).options.defaultTimestamp; - case BATCH: - return ((Requests.Batch) this).options.defaultTimestamp; - default: - return 0; - } - } - - ByteBuffer pagingState() { - switch (this.type) { - case QUERY: - return ((Requests.Query) this).options.pagingState; - case EXECUTE: - return ((Requests.Execute) this).options.pagingState; - default: - return null; - } - } - - Request copy() { - Request request = copyInternal(); - request.setCustomPayload(this.getCustomPayload()); - return request; - } - - protected abstract Request copyInternal(); - - Request copy(ConsistencyLevel newConsistencyLevel) { - Request request = copyInternal(newConsistencyLevel); - request.setCustomPayload(this.getCustomPayload()); - return request; - } - - protected Request copyInternal(ConsistencyLevel newConsistencyLevel) { - throw new UnsupportedOperationException(); - } - } - - abstract static class Response extends Message { - - enum Type { - ERROR(0, Responses.Error.decoder), - READY(2, Responses.Ready.decoder), - AUTHENTICATE(3, Responses.Authenticate.decoder), - SUPPORTED(6, Responses.Supported.decoder), - RESULT(8, Responses.Result.decoder), - EVENT(12, Responses.Event.decoder), - AUTH_CHALLENGE(14, Responses.AuthChallenge.decoder), - AUTH_SUCCESS(16, Responses.AuthSuccess.decoder); - - final int opcode; - final Decoder decoder; - - private static final Type[] opcodeIdx; - - static { - int maxOpcode = -1; - for (Type type : Type.values()) maxOpcode = Math.max(maxOpcode, type.opcode); - opcodeIdx = new Type[maxOpcode + 1]; - for (Type type : Type.values()) { - if (opcodeIdx[type.opcode] != null) throw new IllegalStateException("Duplicate opcode"); - opcodeIdx[type.opcode] = type; - } - } - - Type(int opcode, Decoder decoder) { - this.opcode = opcode; - this.decoder = decoder; - } - - static Type fromOpcode(int opcode) { - if (opcode < 0 || opcode >= opcodeIdx.length) - throw new DriverInternalError(String.format("Unknown response opcode %d", opcode)); - Type t = opcodeIdx[opcode]; - if (t == null) - throw new DriverInternalError(String.format("Unknown response opcode %d", opcode)); - return t; - } - } - - final Type type; - protected volatile UUID tracingId; - protected volatile List warnings; - - protected Response(Type type) { - this.type = type; - } - - Response setTracingId(UUID tracingId) { - this.tracingId = tracingId; - return this; - } - - UUID getTracingId() { - return tracingId; - } - - Response setWarnings(List warnings) { - this.warnings = warnings; - return this; - } - } - - @ChannelHandler.Sharable - static class ProtocolDecoder extends MessageToMessageDecoder { - - @Override - protected void decode(ChannelHandlerContext ctx, Frame frame, List out) - throws Exception { - boolean isTracing = frame.header.flags.contains(Frame.Header.Flag.TRACING); - boolean isCustomPayload = frame.header.flags.contains(Frame.Header.Flag.CUSTOM_PAYLOAD); - UUID tracingId = isTracing ? CBUtil.readUUID(frame.body) : null; - Map customPayload = - isCustomPayload ? CBUtil.readBytesMap(frame.body) : null; - - if (customPayload != null && logger.isTraceEnabled()) { - logger.trace( - "Received payload: {} ({} bytes total)", - printPayload(customPayload), - CBUtil.sizeOfBytesMap(customPayload)); - } - - boolean hasWarnings = frame.header.flags.contains(Frame.Header.Flag.WARNING); - List warnings = - hasWarnings ? CBUtil.readStringList(frame.body) : Collections.emptyList(); - - try { - CodecRegistry codecRegistry = ctx.channel().attr(CODEC_REGISTRY_ATTRIBUTE_KEY).get(); - assert codecRegistry != null; - Response response = - Response.Type.fromOpcode(frame.header.opcode) - .decoder - .decode(frame.body, frame.header.version, codecRegistry); - response - .setTracingId(tracingId) - .setWarnings(warnings) - .setCustomPayload(customPayload) - .setStreamId(frame.header.streamId); - out.add(response); - } finally { - frame.body.release(); - } - } - } - - @ChannelHandler.Sharable - static class ProtocolEncoder extends MessageToMessageEncoder { - - final ProtocolVersion protocolVersion; - - ProtocolEncoder(ProtocolVersion version) { - this.protocolVersion = version; - } - - @Override - protected void encode(ChannelHandlerContext ctx, Request request, List out) { - EnumSet flags = computeFlags(request); - int messageSize = encodedSize(request); - ByteBuf body = ctx.alloc().buffer(messageSize); - encode(request, body); - - if (body.capacity() != messageSize) { - logger.debug( - "Detected buffer resizing while encoding {} message ({} => {}), " - + "this is a driver bug " - + "(ultimately it does not affect the query, but leads to a small inefficiency)", - request.type, - messageSize, - body.capacity()); - } - out.add( - Frame.create(protocolVersion, request.type.opcode, request.getStreamId(), flags, body)); - } - - EnumSet computeFlags(Request request) { - EnumSet flags = EnumSet.noneOf(Frame.Header.Flag.class); - if (request.isTracingRequested()) flags.add(Frame.Header.Flag.TRACING); - if (protocolVersion == ProtocolVersion.NEWEST_BETA) flags.add(Frame.Header.Flag.USE_BETA); - Map customPayload = request.getCustomPayload(); - if (customPayload != null) { - if (protocolVersion.compareTo(ProtocolVersion.V4) < 0) - throw new UnsupportedFeatureException( - protocolVersion, "Custom payloads are only supported since native protocol V4"); - flags.add(Frame.Header.Flag.CUSTOM_PAYLOAD); - } - return flags; - } - - int encodedSize(Request request) { - @SuppressWarnings("unchecked") - Coder coder = (Coder) request.type.coder; - int messageSize = coder.encodedSize(request, protocolVersion); - int payloadLength = -1; - if (request.getCustomPayload() != null) { - payloadLength = CBUtil.sizeOfBytesMap(request.getCustomPayload()); - messageSize += payloadLength; - } - return messageSize; - } - - void encode(Request request, ByteBuf destination) { - @SuppressWarnings("unchecked") - Coder coder = (Coder) request.type.coder; - - Map customPayload = request.getCustomPayload(); - if (customPayload != null) { - CBUtil.writeBytesMap(customPayload, destination); - if (logger.isTraceEnabled()) { - logger.trace( - "Sending payload: {} ({} bytes total)", - printPayload(customPayload), - CBUtil.sizeOfBytesMap(customPayload)); - } - } - - coder.encode(request, destination, protocolVersion); - } - } - - // private stuff to debug custom payloads - - private static final char[] hexArray = "0123456789ABCDEF".toCharArray(); - - static String printPayload(Map customPayload) { - if (customPayload == null) return "null"; - if (customPayload.isEmpty()) return "{}"; - StringBuilder sb = new StringBuilder("{"); - Iterator> iterator = customPayload.entrySet().iterator(); - while (iterator.hasNext()) { - Map.Entry entry = iterator.next(); - sb.append(entry.getKey()); - sb.append(":"); - if (entry.getValue() == null) sb.append("null"); - else bytesToHex(entry.getValue(), sb); - if (iterator.hasNext()) sb.append(", "); - } - sb.append("}"); - return sb.toString(); - } - - // this method doesn't modify the given ByteBuffer - static void bytesToHex(ByteBuffer bytes, StringBuilder sb) { - int length = Math.min(bytes.remaining(), 50); - sb.append("0x"); - for (int i = 0; i < length; i++) { - int v = bytes.get(i) & 0xFF; - sb.append(hexArray[v >>> 4]); - sb.append(hexArray[v & 0x0F]); - } - if (bytes.remaining() > 50) sb.append("... [TRUNCATED]"); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/MessageToSegmentEncoder.java b/driver-core/src/main/java/com/datastax/driver/core/MessageToSegmentEncoder.java deleted file mode 100644 index 41924e6c173..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/MessageToSegmentEncoder.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelOutboundHandlerAdapter; -import io.netty.channel.ChannelPromise; - -class MessageToSegmentEncoder extends ChannelOutboundHandlerAdapter { - - private final ByteBufAllocator allocator; - private final Message.ProtocolEncoder requestEncoder; - - private SegmentBuilder segmentBuilder; - - MessageToSegmentEncoder(ByteBufAllocator allocator, Message.ProtocolEncoder requestEncoder) { - this.allocator = allocator; - this.requestEncoder = requestEncoder; - } - - @Override - public void handlerAdded(ChannelHandlerContext ctx) throws Exception { - super.handlerAdded(ctx); - this.segmentBuilder = new SegmentBuilder(ctx, allocator, requestEncoder); - } - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) - throws Exception { - if (msg instanceof Message.Request) { - segmentBuilder.addRequest(((Message.Request) msg), promise); - } else { - super.write(ctx, msg, promise); - } - } - - @Override - public void flush(ChannelHandlerContext ctx) throws Exception { - segmentBuilder.flush(); - super.flush(ctx); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Metadata.java b/driver-core/src/main/java/com/datastax/driver/core/Metadata.java deleted file mode 100644 index 3bd4f9c31a0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Metadata.java +++ /dev/null @@ -1,985 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Maps; -import io.netty.util.collection.IntObjectHashMap; -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.locks.ReentrantLock; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Keeps metadata on the connected cluster, including known nodes and schema definitions. */ -public class Metadata { - - private static final Logger logger = LoggerFactory.getLogger(Metadata.class); - - final Cluster.Manager cluster; - volatile String clusterName; - volatile String partitioner; - // Holds the contact points until we have a connection to the cluster - private final List contactPoints = new CopyOnWriteArrayList(); - // The hosts, keyed by their host_id - private final ConcurrentMap hosts = new ConcurrentHashMap(); - final ConcurrentMap keyspaces = - new ConcurrentHashMap(); - private volatile TokenMap tokenMap; - - final ReentrantLock lock = new ReentrantLock(); - - // See https://github.com/apache/cassandra/blob/trunk/doc/cql3/CQL.textile#appendixA - private static final IntObjectHashMap> RESERVED_KEYWORDS = - indexByCaseInsensitiveHash( - "add", - "allow", - "alter", - "and", - "any", - "apply", - "asc", - "authorize", - "batch", - "begin", - "by", - "columnfamily", - "create", - "delete", - "desc", - "drop", - "each_quorum", - "from", - "grant", - "in", - "index", - "inet", - "infinity", - "insert", - "into", - "keyspace", - "keyspaces", - "limit", - "local_one", - "local_quorum", - "modify", - "nan", - "norecursive", - "of", - "on", - "one", - "order", - "password", - "primary", - "quorum", - "rename", - "revoke", - "schema", - "select", - "set", - "table", - "to", - "token", - "three", - "truncate", - "two", - "unlogged", - "update", - "use", - "using", - "where", - "with"); - - Metadata(Cluster.Manager cluster) { - this.cluster = cluster; - } - - // rebuilds the token map with the current hosts, typically when refreshing schema metadata - void rebuildTokenMap() { - lock.lock(); - try { - if (tokenMap == null) return; - this.tokenMap = - TokenMap.build( - tokenMap.factory, - tokenMap.primaryToTokens, - keyspaces.values(), - tokenMap.ring, - tokenMap.tokenRanges, - tokenMap.tokenToPrimary); - } finally { - lock.unlock(); - } - } - - // rebuilds the token map for a new set of hosts, typically when refreshing nodes list - void rebuildTokenMap(Token.Factory factory, Map> allTokens) { - lock.lock(); - try { - this.tokenMap = TokenMap.build(factory, allTokens, keyspaces.values()); - } finally { - lock.unlock(); - } - } - - Host newHost(EndPoint endPoint) { - return new Host(endPoint, cluster.convictionPolicyFactory, cluster); - } - - void addContactPoint(EndPoint contactPoint) { - contactPoints.add(newHost(contactPoint)); - } - - List getContactPoints() { - return contactPoints; - } - - Host getContactPoint(EndPoint endPoint) { - for (Host host : contactPoints) { - if (host.getEndPoint().equals(endPoint)) { - return host; - } - } - return null; - } - - /** - * @return the previous host associated with this id, or {@code null} if there was no such host. - */ - Host addIfAbsent(Host host) { - return hosts.putIfAbsent(host.getHostId(), host); - } - - boolean remove(Host host) { - return hosts.remove(host.getHostId()) != null; - } - - Host getHost(UUID hostId) { - return hosts.get(hostId); - } - - /** - * @param broadcastRpcAddress the untranslated broadcast RPC address, as indicated in - * server events. - */ - Host getHost(InetSocketAddress broadcastRpcAddress) { - for (Host host : hosts.values()) { - if (broadcastRpcAddress.equals(host.getBroadcastRpcAddress())) { - return host; - } - } - return null; - } - - Host getHost(EndPoint endPoint) { - for (Host host : hosts.values()) { - if (host.getEndPoint().equals(endPoint)) { - return host; - } - } - return null; - } - - // For internal use only - Collection allHosts() { - return hosts.values(); - } - - /* - * Deal with case sensitivity for a given element id (keyspace, table, column, etc.) - * - * This method is used to convert identifiers provided by the client (through methods such as getKeyspace(String)), - * to the format used internally by the driver. - * - * We expect client-facing APIs to behave like cqlsh, that is: - * - identifiers that are mixed-case or contain special characters should be quoted. - * - unquoted identifiers will be lowercased: getKeyspace("Foo") will look for a keyspace named "foo" - */ - static String handleId(String id) { - // Shouldn't really happen for this method, but no reason to fail here - if (id == null) return null; - - boolean isAlphanumericLowCase = true; - boolean isAlphanumeric = true; - for (int i = 0; i < id.length(); i++) { - char c = id.charAt(i); - if (c >= 65 && c <= 90) { // A-Z - isAlphanumericLowCase = false; - } else if (!((c >= 48 && c <= 57) // 0-9 - || (c == 95) // _ (underscore) - || (c >= 97 && c <= 122) // a-z - )) { - isAlphanumeric = false; - isAlphanumericLowCase = false; - break; - } - } - - if (isAlphanumericLowCase) { - return id; - } - if (isAlphanumeric) { - return id.toLowerCase(); - } - - // Check if it's enclosed in quotes. If it is, remove them and unescape internal double quotes - return ParseUtils.unDoubleQuote(id); - } - - /** - * Quotes a CQL identifier if necessary. - * - *

This is similar to {@link #quote(String)}, except that it won't quote the input string if it - * can safely be used as-is. For example: - * - *

    - *
  • {@code quoteIfNecessary("foo").equals("foo")} (no need to quote). - *
  • {@code quoteIfNecessary("Foo").equals("\"Foo\"")} (identifier is mixed case so case - * sensitivity is required) - *
  • {@code quoteIfNecessary("foo bar").equals("\"foo bar\"")} (identifier contains special - * characters) - *
  • {@code quoteIfNecessary("table").equals("\"table\"")} (identifier is a reserved CQL - * keyword) - *
- * - * @param id the "internal" form of the identifier. That is, the identifier as it would appear in - * Cassandra system tables (such as {@code system_schema.tables}, {@code - * system_schema.columns}, etc.) - * @return the identifier as it would appear in a CQL query string. This is also how you need to - * pass it to public driver methods, such as {@link #getKeyspace(String)}. - */ - public static String quoteIfNecessary(String id) { - return needsQuote(id) ? quote(id) : id; - } - - /** - * We don't need to escape an identifier if it matches non-quoted CQL3 ids ([a-z][a-z0-9_]*), and - * if it's not a CQL reserved keyword. - * - *

When 'Migrating from compact storage' after DROP COMPACT STORAGE on the table, it can have a - * column with an empty name. (See JAVA-2174 for the reference) For that case, we need to escape - * empty column name. - */ - private static boolean needsQuote(String s) { - // this method should only be called for C*-provided identifiers, - // so we expect it to be non-null - assert s != null; - if (s.isEmpty()) return true; - char c = s.charAt(0); - if (!(c >= 97 && c <= 122)) // a-z - return true; - for (int i = 1; i < s.length(); i++) { - c = s.charAt(i); - if (!((c >= 48 && c <= 57) // 0-9 - || (c == 95) // _ - || (c >= 97 && c <= 122) // a-z - )) { - return true; - } - } - return isReservedCqlKeyword(s); - } - - /** - * Builds the internal name of a function/aggregate, which is similar, but not identical, to the - * function/aggregate signature. This is only used to generate keys for internal metadata maps - * (KeyspaceMetadata.functions and. KeyspaceMetadata.aggregates). Note that if simpleName comes - * from the user, the caller must call handleId on it before passing it to this method. Note that - * this method does not necessarily generates a valid CQL function signature. Note that - * argumentTypes can be either a list of strings (schema change events) or a list of DataTypes - * (function lookup from client code). This method must ensure that both cases produce the same - * identifier. - */ - static String fullFunctionName(String simpleName, Collection argumentTypes) { - StringBuilder sb = new StringBuilder(simpleName); - sb.append('('); - boolean first = true; - for (Object argumentType : argumentTypes) { - if (first) first = false; - else sb.append(','); - // user types must be represented by their names only, - // without keyspace prefix, because that's how - // they appear in a schema change event (in targetSignature) - if (argumentType instanceof UserType) { - UserType userType = (UserType) argumentType; - String typeName = Metadata.quoteIfNecessary(userType.getTypeName()); - sb.append(typeName); - } else { - sb.append(argumentType); - } - } - sb.append(')'); - return sb.toString(); - } - - /** - * Quote a keyspace, table or column identifier to make it case sensitive. - * - *

CQL identifiers, including keyspace, table and column ones, are case insensitive by default. - * Case sensitive identifiers can however be provided by enclosing the identifier in double quotes - * (see the CQL - * documentation for details). If you are using case sensitive identifiers, this method can be - * used to enclose such identifiers in double quotes, making them case sensitive. - * - *

Note that reserved CQL - * keywords should also be quoted. You can check if a given identifier is a reserved keyword - * by calling {@link #isReservedCqlKeyword(String)}. - * - * @param id the keyspace or table identifier. - * @return {@code id} enclosed in double-quotes, for use in methods like {@link #getReplicas}, - * {@link #getKeyspace}, {@link KeyspaceMetadata#getTable} or even {@link - * Cluster#connect(String)}. - */ - public static String quote(String id) { - return ParseUtils.doubleQuote(id); - } - - /** - * Checks whether an identifier is a known reserved CQL keyword or not. - * - *

The check is case-insensitive, i.e., the word "{@code KeYsPaCe}" would be considered as a - * reserved CQL keyword just as "{@code keyspace}". - * - *

Note: The list of reserved CQL keywords is subject to change in future versions of - * Cassandra. As a consequence, this method is provided solely as a convenience utility and should - * not be considered as an authoritative source of truth for checking reserved CQL keywords. - * - * @param id the identifier to check; should not be {@code null}. - * @return {@code true} if the given identifier is a known reserved CQL keyword, {@code false} - * otherwise. - */ - public static boolean isReservedCqlKeyword(String id) { - if (id == null) { - return false; - } - int hash = caseInsensitiveHash(id); - List keywords = RESERVED_KEYWORDS.get(hash); - if (keywords == null) { - return false; - } else { - for (char[] keyword : keywords) { - if (equalsIgnoreCaseAscii(id, keyword)) { - return true; - } - } - return false; - } - } - - private static int caseInsensitiveHash(String str) { - int hashCode = 17; - for (int i = 0; i < str.length(); i++) { - char c = toLowerCaseAscii(str.charAt(i)); - hashCode = 31 * hashCode + c; - } - return hashCode; - } - - // keyword is expected as a second argument always in low case - private static boolean equalsIgnoreCaseAscii(String str1, char[] str2LowCase) { - if (str1.length() != str2LowCase.length) return false; - - for (int i = 0; i < str1.length(); i++) { - char c1 = str1.charAt(i); - char c2Low = str2LowCase[i]; - if (c1 == c2Low) { - continue; - } - char low1 = toLowerCaseAscii(c1); - if (low1 == c2Low) { - continue; - } - return false; - } - return true; - } - - private static char toLowerCaseAscii(char c) { - if (c >= 65 && c <= 90) { // A-Z - c ^= 0x20; // convert to low case - } - return c; - } - - private static IntObjectHashMap> indexByCaseInsensitiveHash(String... words) { - IntObjectHashMap> result = new IntObjectHashMap>(); - for (String word : words) { - char[] wordAsCharArray = word.toLowerCase().toCharArray(); - int hash = caseInsensitiveHash(word); - List list = result.get(hash); - if (list == null) { - list = new ArrayList(); - result.put(hash, list); - } - list.add(wordAsCharArray); - } - return result; - } - - /** - * Returns the token ranges that define data distribution in the ring. - * - *

Note that this information is refreshed asynchronously by the control connection, when - * schema or ring topology changes. It might occasionally be stale. - * - * @return the token ranges. Note that the result might be stale or empty if metadata was - * explicitly disabled with {@link QueryOptions#setMetadataEnabled(boolean)}. - */ - public Set getTokenRanges() { - TokenMap current = tokenMap; - return (current == null) ? Collections.emptySet() : current.tokenRanges; - } - - /** - * Returns the token ranges that are replicated on the given host, for the given keyspace. - * - *

Note that this information is refreshed asynchronously by the control connection, when - * schema or ring topology changes. It might occasionally be stale (or even empty). - * - * @param keyspace the name of the keyspace to get token ranges for. - * @param host the host. - * @return the (immutable) set of token ranges for {@code host} as known by the driver. Note that - * the result might be stale or empty if metadata was explicitly disabled with {@link - * QueryOptions#setMetadataEnabled(boolean)}. - */ - public Set getTokenRanges(String keyspace, Host host) { - keyspace = handleId(keyspace); - TokenMap current = tokenMap; - if (current == null) { - return Collections.emptySet(); - } else { - Map> dcRanges = current.hostsToRangesByKeyspace.get(keyspace); - if (dcRanges == null) { - return Collections.emptySet(); - } else { - Set ranges = dcRanges.get(host); - return (ranges == null) ? Collections.emptySet() : ranges; - } - } - } - - /** - * Returns the set of hosts that are replica for a given partition key. - * - *

Note that this information is refreshed asynchronously by the control connection, when - * schema or ring topology changes. It might occasionally be stale (or even empty). - * - * @param keyspace the name of the keyspace to get replicas for. - * @param partitionKey the partition key for which to find the set of replica. - * @return the (immutable) set of replicas for {@code partitionKey} as known by the driver. Note - * that the result might be stale or empty if metadata was explicitly disabled with {@link - * QueryOptions#setMetadataEnabled(boolean)}. - */ - public Set getReplicas(String keyspace, ByteBuffer partitionKey) { - keyspace = handleId(keyspace); - TokenMap current = tokenMap; - if (current == null) { - return Collections.emptySet(); - } else { - Set hosts = current.getReplicas(keyspace, current.factory.hash(partitionKey)); - return hosts == null ? Collections.emptySet() : hosts; - } - } - - /** - * Returns the set of hosts that are replica for a given token range. - * - *

Note that it is assumed that the input range does not overlap across multiple host ranges. - * If the range extends over multiple hosts, it only returns the replicas for those hosts that are - * replicas for the last token of the range. This behavior may change in a future release, see JAVA-1355. - * - *

Also note that this information is refreshed asynchronously by the control connection, when - * schema or ring topology changes. It might occasionally be stale (or even empty). - * - * @param keyspace the name of the keyspace to get replicas for. - * @param range the token range. - * @return the (immutable) set of replicas for {@code range} as known by the driver. Note that the - * result might be stale or empty if metadata was explicitly disabled with {@link - * QueryOptions#setMetadataEnabled(boolean)}. - */ - public Set getReplicas(String keyspace, TokenRange range) { - keyspace = handleId(keyspace); - TokenMap current = tokenMap; - if (current == null) { - return Collections.emptySet(); - } else { - Set hosts = current.getReplicas(keyspace, range.getEnd()); - return hosts == null ? Collections.emptySet() : hosts; - } - } - - /** - * The Cassandra name for the cluster connect to. - * - * @return the Cassandra name for the cluster connect to. - */ - public String getClusterName() { - return clusterName; - } - - /** - * The partitioner in use as reported by the Cassandra nodes. - * - * @return the partitioner in use as reported by the Cassandra nodes. - */ - public String getPartitioner() { - return partitioner; - } - - /** - * Returns the known hosts of this cluster. - * - * @return A set will all the know host of this cluster. - */ - public Set getAllHosts() { - return new HashSet(allHosts()); - } - - /** - * Checks whether hosts that are currently up agree on the schema definition. - * - *

This method performs a one-time check only, without any form of retry; therefore {@link - * Cluster.Builder#withMaxSchemaAgreementWaitSeconds(int)} does not apply in this case. - * - * @return {@code true} if all hosts agree on the schema; {@code false} if they don't agree, or if - * the check could not be performed (for example, if the control connection is down). - */ - public boolean checkSchemaAgreement() { - try { - return cluster.controlConnection.checkSchemaAgreement(); - } catch (Exception e) { - logger.warn("Error while checking schema agreement", e); - return false; - } - } - - /** - * Returns the metadata of a keyspace given its name. - * - * @param keyspace the name of the keyspace for which metadata should be returned. - * @return the metadata of the requested keyspace or {@code null} if {@code keyspace} is not a - * known keyspace. Note that the result might be stale or null if metadata was explicitly - * disabled with {@link QueryOptions#setMetadataEnabled(boolean)}. - */ - public KeyspaceMetadata getKeyspace(String keyspace) { - return keyspaces.get(handleId(keyspace)); - } - - KeyspaceMetadata removeKeyspace(String keyspace) { - KeyspaceMetadata removed = keyspaces.remove(keyspace); - if (tokenMap != null) tokenMap.tokenToHostsByKeyspace.remove(keyspace); - return removed; - } - - /** - * Returns a list of all the defined keyspaces. - * - * @return a list of all the defined keyspaces. Note that the result might be stale or empty if - * metadata was explicitly disabled with {@link QueryOptions#setMetadataEnabled(boolean)}. - */ - public List getKeyspaces() { - return new ArrayList(keyspaces.values()); - } - - /** - * Returns a {@code String} containing CQL queries representing the schema of this cluster. - * - *

In other words, this method returns the queries that would allow to recreate the schema of - * this cluster. - * - *

Note that the returned String is formatted to be human readable (for some definition of - * human readable at least). - * - *

It might be stale or empty if metadata was explicitly disabled with {@link - * QueryOptions#setMetadataEnabled(boolean)}. - * - * @return the CQL queries representing this cluster schema as a {code String}. - */ - public String exportSchemaAsString() { - StringBuilder sb = new StringBuilder(); - - for (KeyspaceMetadata ksm : keyspaces.values()) sb.append(ksm.exportAsString()).append('\n'); - - return sb.toString(); - } - - /** - * Creates a tuple type given a list of types. - * - * @param types the types for the tuple type. - * @return the newly created tuple type. - */ - public TupleType newTupleType(DataType... types) { - return newTupleType(Arrays.asList(types)); - } - - /** - * Creates a tuple type given a list of types. - * - * @param types the types for the tuple type. - * @return the newly created tuple type. - */ - public TupleType newTupleType(List types) { - return new TupleType( - types, cluster.protocolVersion(), cluster.configuration.getCodecRegistry()); - } - - /** - * Builds a new {@link Token} from its string representation, according to the partitioner - * reported by the Cassandra nodes. - * - * @param tokenStr the string representation. - * @return the token. - * @throws IllegalStateException if the token factory was not initialized. This would typically - * happen if metadata was explicitly disabled with {@link - * QueryOptions#setMetadataEnabled(boolean)} before startup. - */ - public Token newToken(String tokenStr) { - TokenMap current = tokenMap; - if (current == null) - throw new IllegalStateException( - "Token factory not set. This should only happen if metadata was explicitly disabled"); - return current.factory.fromString(tokenStr); - } - - /** - * Builds a new {@link Token} from a partition key. - * - * @param components the components of the partition key, in their serialized form (obtained with - * {@link TypeCodec#serialize(Object, ProtocolVersion)}). - * @return the token. - * @throws IllegalStateException if the token factory was not initialized. This would typically - * happen if metadata was explicitly disabled with {@link - * QueryOptions#setMetadataEnabled(boolean)} before startup. - */ - public Token newToken(ByteBuffer... components) { - TokenMap current = tokenMap; - if (current == null) - throw new IllegalStateException( - "Token factory not set. This should only happen if metadata was explicitly disabled"); - return current.factory.hash(SimpleStatement.compose(components)); - } - - /** - * Builds a new {@link TokenRange}. - * - * @param start the start token. - * @param end the end token. - * @return the range. - * @throws IllegalStateException if the token factory was not initialized. This would typically - * happen if metadata was explicitly disabled with {@link - * QueryOptions#setMetadataEnabled(boolean)} before startup. - */ - public TokenRange newTokenRange(Token start, Token end) { - TokenMap current = tokenMap; - if (current == null) - throw new IllegalStateException( - "Token factory not set. This should only happen if metadata was explicitly disabled"); - - return new TokenRange(start, end, current.factory); - } - - Token.Factory tokenFactory() { - TokenMap current = tokenMap; - return (current == null) ? null : current.factory; - } - - void triggerOnKeyspaceAdded(KeyspaceMetadata keyspace) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onKeyspaceAdded(keyspace); - } - } - - void triggerOnKeyspaceChanged(KeyspaceMetadata current, KeyspaceMetadata previous) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onKeyspaceChanged(current, previous); - } - } - - void triggerOnKeyspaceRemoved(KeyspaceMetadata keyspace) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onKeyspaceRemoved(keyspace); - } - } - - void triggerOnTableAdded(TableMetadata table) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onTableAdded(table); - } - } - - void triggerOnTableChanged(TableMetadata current, TableMetadata previous) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onTableChanged(current, previous); - } - } - - void triggerOnTableRemoved(TableMetadata table) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onTableRemoved(table); - } - } - - void triggerOnUserTypeAdded(UserType type) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onUserTypeAdded(type); - } - } - - void triggerOnUserTypeChanged(UserType current, UserType previous) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onUserTypeChanged(current, previous); - } - } - - void triggerOnUserTypeRemoved(UserType type) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onUserTypeRemoved(type); - } - } - - void triggerOnFunctionAdded(FunctionMetadata function) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onFunctionAdded(function); - } - } - - void triggerOnFunctionChanged(FunctionMetadata current, FunctionMetadata previous) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onFunctionChanged(current, previous); - } - } - - void triggerOnFunctionRemoved(FunctionMetadata function) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onFunctionRemoved(function); - } - } - - void triggerOnAggregateAdded(AggregateMetadata aggregate) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onAggregateAdded(aggregate); - } - } - - void triggerOnAggregateChanged(AggregateMetadata current, AggregateMetadata previous) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onAggregateChanged(current, previous); - } - } - - void triggerOnAggregateRemoved(AggregateMetadata aggregate) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onAggregateRemoved(aggregate); - } - } - - void triggerOnMaterializedViewAdded(MaterializedViewMetadata view) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onMaterializedViewAdded(view); - } - } - - void triggerOnMaterializedViewChanged( - MaterializedViewMetadata current, MaterializedViewMetadata previous) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onMaterializedViewChanged(current, previous); - } - } - - void triggerOnMaterializedViewRemoved(MaterializedViewMetadata view) { - for (SchemaChangeListener listener : cluster.schemaChangeListeners) { - listener.onMaterializedViewRemoved(view); - } - } - - private static class TokenMap { - - private final Token.Factory factory; - private final Map> primaryToTokens; - private final Map>> tokenToHostsByKeyspace; - private final Map>> hostsToRangesByKeyspace; - private final List ring; - private final Set tokenRanges; - private final Map tokenToPrimary; - - private TokenMap( - Token.Factory factory, - List ring, - Set tokenRanges, - Map tokenToPrimary, - Map> primaryToTokens, - Map>> tokenToHostsByKeyspace, - Map>> hostsToRangesByKeyspace) { - this.factory = factory; - this.ring = ring; - this.tokenRanges = tokenRanges; - this.tokenToPrimary = tokenToPrimary; - this.primaryToTokens = primaryToTokens; - this.tokenToHostsByKeyspace = tokenToHostsByKeyspace; - this.hostsToRangesByKeyspace = hostsToRangesByKeyspace; - for (Map.Entry> entry : primaryToTokens.entrySet()) { - Host host = entry.getKey(); - host.setTokens(ImmutableSet.copyOf(entry.getValue())); - } - } - - private static TokenMap build( - Token.Factory factory, - Map> allTokens, - Collection keyspaces) { - Map tokenToPrimary = new HashMap(); - Set allSorted = new TreeSet(); - for (Map.Entry> entry : allTokens.entrySet()) { - Host host = entry.getKey(); - for (Token t : entry.getValue()) { - try { - allSorted.add(t); - tokenToPrimary.put(t, host); - } catch (IllegalArgumentException e) { - // If we failed parsing that token, skip it - } - } - } - List ring = new ArrayList(allSorted); - Set tokenRanges = makeTokenRanges(ring, factory); - return build(factory, allTokens, keyspaces, ring, tokenRanges, tokenToPrimary); - } - - private static TokenMap build( - Token.Factory factory, - Map> allTokens, - Collection keyspaces, - List ring, - Set tokenRanges, - Map tokenToPrimary) { - Set hosts = allTokens.keySet(); - Map>> tokenToHosts = - new HashMap>>(); - Map>> replStrategyToHosts = - new HashMap>>(); - Map>> hostsToRanges = - new HashMap>>(); - for (KeyspaceMetadata keyspace : keyspaces) { - ReplicationStrategy strategy = keyspace.replicationStrategy(); - Map> ksTokens = replStrategyToHosts.get(strategy); - if (ksTokens == null) { - ksTokens = - (strategy == null) - ? makeNonReplicatedMap(tokenToPrimary) - : strategy.computeTokenToReplicaMap(keyspace.getName(), tokenToPrimary, ring); - replStrategyToHosts.put(strategy, ksTokens); - } - - tokenToHosts.put(keyspace.getName(), ksTokens); - - Map> ksRanges; - if (ring.size() == 1) { - // We forced the single range to ]minToken,minToken], make sure to use that instead of - // relying on the host's token - ImmutableMap.Builder> builder = ImmutableMap.builder(); - for (Host host : allTokens.keySet()) builder.put(host, tokenRanges); - ksRanges = builder.build(); - } else { - ksRanges = computeHostsToRangesMap(tokenRanges, ksTokens, hosts.size()); - } - hostsToRanges.put(keyspace.getName(), ksRanges); - } - return new TokenMap( - factory, ring, tokenRanges, tokenToPrimary, allTokens, tokenToHosts, hostsToRanges); - } - - private Set getReplicas(String keyspace, Token token) { - - Map> tokenToHosts = tokenToHostsByKeyspace.get(keyspace); - if (tokenToHosts == null) return Collections.emptySet(); - - // If the token happens to be one of the "primary" tokens, get result directly - Set hosts = tokenToHosts.get(token); - if (hosts != null) return hosts; - - // Otherwise, find closest "primary" token on the ring - int i = Collections.binarySearch(ring, token); - if (i < 0) { - i = -i - 1; - if (i >= ring.size()) i = 0; - } - - return tokenToHosts.get(ring.get(i)); - } - - private static Map> makeNonReplicatedMap(Map input) { - Map> output = new HashMap>(input.size()); - for (Map.Entry entry : input.entrySet()) - output.put(entry.getKey(), ImmutableSet.of(entry.getValue())); - return output; - } - - private static Set makeTokenRanges(List ring, Token.Factory factory) { - ImmutableSet.Builder builder = ImmutableSet.builder(); - // JAVA-684: if there is only one token, return the range ]minToken, minToken] - if (ring.size() == 1) { - builder.add(new TokenRange(factory.minToken(), factory.minToken(), factory)); - } else { - for (int i = 0; i < ring.size(); i++) { - Token start = ring.get(i); - Token end = ring.get((i + 1) % ring.size()); - builder.add(new TokenRange(start, end, factory)); - } - } - return builder.build(); - } - - private static Map> computeHostsToRangesMap( - Set tokenRanges, Map> ksTokens, int hostCount) { - Map> builders = - Maps.newHashMapWithExpectedSize(hostCount); - for (TokenRange range : tokenRanges) { - Set replicas = ksTokens.get(range.getEnd()); - for (Host host : replicas) { - ImmutableSet.Builder hostRanges = builders.get(host); - if (hostRanges == null) { - hostRanges = ImmutableSet.builder(); - builders.put(host, hostRanges); - } - hostRanges.add(range); - } - } - Map> ksRanges = Maps.newHashMapWithExpectedSize(hostCount); - for (Map.Entry> entry : builders.entrySet()) { - ksRanges.put(entry.getKey(), entry.getValue().build()); - } - return ksRanges; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Metrics.java b/driver-core/src/main/java/com/datastax/driver/core/Metrics.java deleted file mode 100644 index 8f4498be490..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Metrics.java +++ /dev/null @@ -1,668 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.codahale.metrics.Counter; -import com.codahale.metrics.Gauge; -import com.codahale.metrics.JmxReporter; -import com.codahale.metrics.Meter; -import com.codahale.metrics.MetricRegistry; -import com.codahale.metrics.Timer; -import com.datastax.driver.core.policies.SpeculativeExecutionPolicy; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; - -/** - * Metrics exposed by the driver. - * - *

The metrics exposed by this class use the Metrics - * library and you should refer its documentation for details on how to handle the exposed metric - * objects. - * - *

By default, metrics are exposed through JMX, which is very useful for development and - * browsing, but for production environments you may want to have a look at the reporters provided by the - * Metrics library which could be more efficient/adapted. - */ -public class Metrics { - - private final Cluster.Manager manager; - private final MetricRegistry registry = new MetricRegistry(); - private final JmxReporter jmxReporter; - private final Errors errors = new Errors(); - - private final Timer requests = registry.timer("requests"); - private final Meter bytesSent = registry.meter("bytes-sent"); - private final Meter bytesReceived = registry.meter("bytes-received"); - - private final Gauge knownHosts = - registry.register( - "known-hosts", - new Gauge() { - @Override - public Integer getValue() { - return manager.metadata.allHosts().size(); - } - }); - private final Gauge connectedTo = - registry.register( - "connected-to", - new Gauge() { - @Override - public Integer getValue() { - Set s = new HashSet(); - for (SessionManager session : manager.sessions) s.addAll(session.pools.keySet()); - return s.size(); - } - }); - private final Gauge openConnections = - registry.register( - "open-connections", - new Gauge() { - @Override - public Integer getValue() { - int value = manager.controlConnection.isOpen() ? 1 : 0; - for (SessionManager session : manager.sessions) - for (HostConnectionPool pool : session.pools.values()) value += pool.opened(); - return value; - } - }); - private final Gauge trashedConnections = - registry.register( - "trashed-connections", - new Gauge() { - @Override - public Integer getValue() { - int value = 0; - for (SessionManager session : manager.sessions) - for (HostConnectionPool pool : session.pools.values()) value += pool.trashed(); - return value; - } - }); - private final Gauge inFlightRequests = - registry.register( - "inflight-requests", - new Gauge() { - @Override - public Integer getValue() { - int value = 0; - for (SessionManager session : manager.sessions) - for (HostConnectionPool pool : session.pools.values()) - value += pool.totalInFlight.get(); - return value; - } - }); - - private final Gauge requestQueueDepth = - registry.register( - "request-queue-depth", - new Gauge() { - @Override - public Integer getValue() { - int value = 0; - for (SessionManager session : manager.sessions) - for (HostConnectionPool pool : session.pools.values()) - value += pool.pendingBorrowCount.get(); - return value; - } - }); - - private final Gauge executorQueueDepth; - private final Gauge blockingExecutorQueueDepth; - private final Gauge reconnectionSchedulerQueueSize; - private final Gauge taskSchedulerQueueSize; - - Metrics(Cluster.Manager manager) { - this.manager = manager; - this.executorQueueDepth = - registry.register("executor-queue-depth", buildQueueSizeGauge(manager.executorQueue)); - this.blockingExecutorQueueDepth = - registry.register( - "blocking-executor-queue-depth", buildQueueSizeGauge(manager.blockingExecutorQueue)); - this.reconnectionSchedulerQueueSize = - registry.register( - "reconnection-scheduler-task-count", - buildQueueSizeGauge(manager.reconnectionExecutorQueue)); - this.taskSchedulerQueueSize = - registry.register( - "task-scheduler-task-count", buildQueueSizeGauge(manager.scheduledTasksExecutorQueue)); - if (manager.configuration.getMetricsOptions().isJMXReportingEnabled()) { - this.jmxReporter = - JmxReporter.forRegistry(registry).inDomain(manager.clusterName + "-metrics").build(); - this.jmxReporter.start(); - } else { - this.jmxReporter = null; - } - } - - /** - * Returns the registry containing all metrics. - * - *

The metrics registry allows you to easily use the reporters that ship with Metrics or a custom - * written one. - * - *

For instance, if {@code metrics} is {@code this} object, you could export the metrics to csv - * files using: - * - *

-   *     com.codahale.metrics.CsvReporter.forRegistry(metrics.getRegistry()).build(new File("measurements/")).start(1, TimeUnit.SECONDS);
-   * 
- * - *

If you already have a {@code MetricRegistry} in your application and wish to add the - * driver's metrics to it, the recommended approach is to use a listener: - * - *

-   *     // Your existing registry:
-   *     final com.codahale.metrics.MetricRegistry myRegistry = ...
-   *
-   *     cluster.getMetrics().getRegistry().addListener(new com.codahale.metrics.MetricRegistryListener() {
-   *         @Override
-   *         public void onGaugeAdded(String name, Gauge<?> gauge) {
-   *             if (myRegistry.getNames().contains(name)) {
-   *                 // name is already taken, maybe prefix with a namespace
-   *                 ...
-   *             } else {
-   *                 myRegistry.register(name, gauge);
-   *             }
-   *         }
-   *
-   *         ... // Implement other methods in a similar fashion
-   *     });
-   * 
- * - * Since reporting is handled by your registry, you'll probably also want to disable JMX reporting - * with {@link Cluster.Builder#withoutJMXReporting()}. - * - * @return the registry containing all metrics. - */ - public MetricRegistry getRegistry() { - return registry; - } - - /** - * Returns metrics on the user requests performed on the Cluster. - * - *

This metric exposes - * - *

    - *
  • the total number of requests. - *
  • the requests rate (in requests per seconds), including 1, 5 and 15 minute rates. - *
  • the mean, min and max latencies, as well as latency at a given percentile. - *
- * - * @return a {@code Timer} metric object exposing the rate and latency for user requests. - */ - public Timer getRequestsTimer() { - return requests; - } - - /** - * Returns an object grouping metrics related to the errors encountered. - * - * @return an object grouping metrics related to the errors encountered. - */ - public Errors getErrorMetrics() { - return errors; - } - - /** - * Returns the number of Cassandra hosts currently known by the driver (that is whether they are - * currently considered up or down). - * - * @return the number of Cassandra hosts currently known by the driver. - */ - public Gauge getKnownHosts() { - return knownHosts; - } - - /** - * Returns the number of Cassandra hosts the driver is currently connected to (that is have at - * least one connection opened to). - * - * @return the number of Cassandra hosts the driver is currently connected to. - */ - public Gauge getConnectedToHosts() { - return connectedTo; - } - - /** - * Returns the total number of currently opened connections to Cassandra hosts. - * - * @return The total number of currently opened connections to Cassandra hosts. - */ - public Gauge getOpenConnections() { - return openConnections; - } - - /** - * Returns the total number of currently "trashed" connections to Cassandra hosts. - * - *

When the load to a host decreases, the driver will reclaim some connections in order to save - * resources. No requests are sent to these connections anymore, but they are kept open for an - * additional amount of time ({@link PoolingOptions#getIdleTimeoutSeconds()}), in case the load - * goes up again. This metric counts connections in that state. - * - * @return The total number of currently trashed connections to Cassandra hosts. - */ - public Gauge getTrashedConnections() { - return trashedConnections; - } - - /** - * Returns the total number of in flight requests to Cassandra hosts. - * - * @return The total number of in flight requests to Cassandra hosts. - */ - public Gauge getInFlightRequests() { - return inFlightRequests; - } - - /** - * Returns the total number of enqueued requests on all Cassandra hosts. - * - * @see Session.State#getRequestQueueDepth(Host) - * @return The total number of enqueued requests on all Cassandra hosts. - */ - public Gauge getRequestQueueDepth() { - return requestQueueDepth; - } - - /** - * Returns the number of queued up tasks in the {@link ThreadingOptions#createExecutor(String) - * main internal executor}. - * - *

If the executor's task queue is not accessible – which happens when the executor is not an - * instance of {@link ThreadPoolExecutor} – then this gauge returns -1. - * - * @return The number of queued up tasks in the main internal executor, or -1, if that number is - * unknown. - */ - public Gauge getExecutorQueueDepth() { - return executorQueueDepth; - } - - /** - * Returns the number of queued up tasks in the {@link - * ThreadingOptions#createBlockingExecutor(String) blocking executor}. - * - *

If the executor's task queue is not accessible – which happens when the executor is not an - * instance of {@link ThreadPoolExecutor} – then this gauge returns -1. - * - * @return The number of queued up tasks in the blocking executor, or -1, if that number is - * unknown. - */ - public Gauge getBlockingExecutorQueueDepth() { - return blockingExecutorQueueDepth; - } - - /** - * Returns the number of queued up tasks in the {@link - * ThreadingOptions#createReconnectionExecutor(String) reconnection executor}. - * - *

A queue size > 0 does not necessarily indicate a backlog as some tasks may not have been - * scheduled to execute yet. - * - *

If the executor's task queue is not accessible – which happens when the executor is not an - * instance of {@link ThreadPoolExecutor} – then this gauge returns -1. - * - * @return The size of the work queue for the reconnection executor, or -1, if that number is - * unknown. - */ - public Gauge getReconnectionSchedulerQueueSize() { - return reconnectionSchedulerQueueSize; - } - - /** - * Returns the number of queued up tasks in the {@link - * ThreadingOptions#createScheduledTasksExecutor(String) scheduled tasks executor}. - * - *

A queue size > 0 does not necessarily indicate a backlog as some tasks may not have been - * scheduled to execute yet. - * - *

If the executor's task queue is not accessible – which happens when the executor is not an - * instance of {@link ThreadPoolExecutor} – then this gauge returns -1. - * - * @return The size of the work queue for the scheduled tasks executor, or -1, if that number is - * unknown. - */ - public Gauge getTaskSchedulerQueueSize() { - return taskSchedulerQueueSize; - } - - /** - * Returns the number of bytes sent so far. - * - *

Note that this measures unencrypted traffic, even if SSL is enabled (the probe is inserted - * before SSL handlers in the Netty pipeline). In practice, SSL overhead should be negligible - * after the initial handshake. - * - * @return the number of bytes sent so far. - */ - public Meter getBytesSent() { - return bytesSent; - } - - /** - * Returns the number of bytes received so far. - * - *

Note that this measures unencrypted traffic, even if SSL is enabled (the probe is inserted - * before SSL handlers in the Netty pipeline). In practice, SSL overhead should be negligible - * after the initial handshake. - * - * @return the number of bytes received so far. - */ - public Meter getBytesReceived() { - return bytesReceived; - } - - void shutdown() { - if (jmxReporter != null) jmxReporter.stop(); - } - - private static Gauge buildQueueSizeGauge(final BlockingQueue queue) { - if (queue != null) { - return new Gauge() { - @Override - public Integer getValue() { - return queue.size(); - } - }; - } else { - return new Gauge() { - @Override - public Integer getValue() { - return -1; - } - }; - } - } - - /** Metrics on errors encountered. */ - public class Errors { - - private final Counter connectionErrors = registry.counter("connection-errors"); - private final Counter authenticationErrors = registry.counter("authentication-errors"); - - private final Counter writeTimeouts = registry.counter("write-timeouts"); - private final Counter readTimeouts = registry.counter("read-timeouts"); - private final Counter unavailables = registry.counter("unavailables"); - private final Counter clientTimeouts = registry.counter("client-timeouts"); - - private final Counter otherErrors = registry.counter("other-errors"); - - private final Counter retries = registry.counter("retries"); - private final Counter retriesOnWriteTimeout = registry.counter("retries-on-write-timeout"); - private final Counter retriesOnReadTimeout = registry.counter("retries-on-read-timeout"); - private final Counter retriesOnUnavailable = registry.counter("retries-on-unavailable"); - private final Counter retriesOnClientTimeout = registry.counter("retries-on-client-timeout"); - private final Counter retriesOnConnectionError = - registry.counter("retries-on-connection-error"); - private final Counter retriesOnOtherErrors = registry.counter("retries-on-other-errors"); - - private final Counter ignores = registry.counter("ignores"); - private final Counter ignoresOnWriteTimeout = registry.counter("ignores-on-write-timeout"); - private final Counter ignoresOnReadTimeout = registry.counter("ignores-on-read-timeout"); - private final Counter ignoresOnUnavailable = registry.counter("ignores-on-unavailable"); - private final Counter ignoresOnClientTimeout = registry.counter("ignores-on-client-timeout"); - private final Counter ignoresOnConnectionError = - registry.counter("ignores-on-connection-error"); - private final Counter ignoresOnOtherErrors = registry.counter("ignores-on-other-errors"); - - private final Counter speculativeExecutions = registry.counter("speculative-executions"); - - /** - * Returns the number of errors while connecting to Cassandra nodes. - * - *

This represents the number of times that a request to a Cassandra node has failed due to a - * connection problem. This thus also corresponds to how often the driver had to pick a fallback - * host for a request. - * - *

You can expect a few connection errors when a Cassandra node fails (or is stopped) ,but if - * that number grows continuously you likely have a problem. - * - * @return the number of errors while connecting to Cassandra nodes. - */ - public Counter getConnectionErrors() { - return connectionErrors; - } - - /** - * Returns the number of authentication errors while connecting to Cassandra nodes. - * - * @return the number of errors. - */ - public Counter getAuthenticationErrors() { - return authenticationErrors; - } - - /** - * Returns the number of write requests that returned a timeout (independently of the final - * decision taken by the {@link com.datastax.driver.core.policies.RetryPolicy}). - * - * @return the number of write timeout. - */ - public Counter getWriteTimeouts() { - return writeTimeouts; - } - - /** - * Returns the number of read requests that returned a timeout (independently of the final - * decision taken by the {@link com.datastax.driver.core.policies.RetryPolicy}). - * - * @return the number of read timeout. - */ - public Counter getReadTimeouts() { - return readTimeouts; - } - - /** - * Returns the number of requests that returned an unavailable exception (independently of the - * final decision taken by the {@link com.datastax.driver.core.policies.RetryPolicy}). - * - * @return the number of unavailable exceptions. - */ - public Counter getUnavailables() { - return unavailables; - } - - /** - * Returns the number of requests that timed out before the driver received a response. - * - * @return the number of client timeouts. - */ - public Counter getClientTimeouts() { - return clientTimeouts; - } - - /** - * Returns the number of requests that returned errors not accounted for by another metric. This - * includes all types of invalid requests. - * - * @return the number of requests errors not accounted by another metric. - */ - public Counter getOthers() { - return otherErrors; - } - - /** - * Returns the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}. - * - * @return the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}. - */ - public Counter getRetries() { - return retries; - } - - /** - * Returns the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a read timed out. - * - * @return the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a read timed out. - */ - public Counter getRetriesOnReadTimeout() { - return retriesOnReadTimeout; - } - - /** - * Returns the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a write timed out. - * - * @return the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a write timed out. - */ - public Counter getRetriesOnWriteTimeout() { - return retriesOnWriteTimeout; - } - - /** - * Returns the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after an unavailable exception. - * - * @return the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after an unavailable exception. - */ - public Counter getRetriesOnUnavailable() { - return retriesOnUnavailable; - } - - /** - * Returns the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a client timeout. - * - * @return the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a client timeout. - */ - public Counter getRetriesOnClientTimeout() { - return retriesOnClientTimeout; - } - - /** - * Returns the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a connection error. - * - * @return the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a connection error. - */ - public Counter getRetriesOnConnectionError() { - return retriesOnConnectionError; - } - - /** - * Returns the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after an unexpected error. - * - * @return the number of times a request was retried due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after an unexpected error. - */ - public Counter getRetriesOnOtherErrors() { - return retriesOnOtherErrors; - } - - /** - * Returns the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, for example due to timeouts or - * unavailability. - * - * @return the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}. - */ - public Counter getIgnores() { - return ignores; - } - - /** - * Returns the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a read timed out. - * - * @return the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a read timed out. - */ - public Counter getIgnoresOnReadTimeout() { - return ignoresOnReadTimeout; - } - - /** - * Returns the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a write timed out. - * - * @return the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a write timed out. - */ - public Counter getIgnoresOnWriteTimeout() { - return ignoresOnWriteTimeout; - } - - /** - * Returns the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after an unavailable exception. - * - * @return the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after an unavailable exception. - */ - public Counter getIgnoresOnUnavailable() { - return ignoresOnUnavailable; - } - - /** - * Returns the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a client timeout. - * - * @return the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a client timeout. - */ - public Counter getIgnoresOnClientTimeout() { - return ignoresOnClientTimeout; - } - - /** - * Returns the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a connection error. - * - * @return the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after a connection error. - */ - public Counter getIgnoresOnConnectionError() { - return ignoresOnConnectionError; - } - - /** - * Returns the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after an unexpected error. - * - * @return the number of times a request was ignored due to the {@link - * com.datastax.driver.core.policies.RetryPolicy}, after an unexpected error. - */ - public Counter getIgnoresOnOtherErrors() { - return ignoresOnOtherErrors; - } - - /** - * Returns the number of times a speculative execution was started because a previous execution - * did not complete within the delay specified by {@link SpeculativeExecutionPolicy}. - * - * @return the number of speculative executions. - */ - public Counter getSpeculativeExecutions() { - return speculativeExecutions; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/MetricsOptions.java b/driver-core/src/main/java/com/datastax/driver/core/MetricsOptions.java deleted file mode 100644 index 71cd5405539..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/MetricsOptions.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** {@link Metrics} options. */ -public class MetricsOptions { - - private final boolean metricsEnabled; - private final boolean jmxEnabled; - - /** - * Creates a new {@code MetricsOptions} object with default values (metrics enabled, JMX reporting - * enabled). - */ - public MetricsOptions() { - this(true, true); - } - - /** - * Creates a new {@code MetricsOptions} object. - * - * @param jmxEnabled whether to enable JMX reporting or not. - */ - public MetricsOptions(boolean enabled, boolean jmxEnabled) { - this.metricsEnabled = enabled; - this.jmxEnabled = jmxEnabled; - } - - /** - * Returns whether metrics are enabled. - * - * @return whether metrics are enabled. - */ - public boolean isEnabled() { - return metricsEnabled; - } - - /** - * Returns whether JMX reporting is enabled. - * - * @return whether JMX reporting is enabled. - */ - public boolean isJMXReportingEnabled() { - return jmxEnabled; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/MetricsUtil.java b/driver-core/src/main/java/com/datastax/driver/core/MetricsUtil.java deleted file mode 100644 index b3252f63a15..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/MetricsUtil.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.net.InetAddress; - -public class MetricsUtil { - - public static String hostMetricName(String prefix, Host host) { - EndPoint endPoint = host.getEndPoint(); - if (endPoint instanceof TranslatedAddressEndPoint) { - InetAddress address = endPoint.resolve().getAddress(); - return hostMetricNameFromAddress(prefix, address); - } else { - // We have no guarantee that endpoints resolve to unique addresses - return prefix + endPoint.toString(); - } - } - - private static String hostMetricNameFromAddress(String prefix, InetAddress address) { - StringBuilder result = new StringBuilder(prefix); - boolean first = true; - for (byte b : address.getAddress()) { - if (first) { - first = false; - } else { - result.append('_'); - } - result.append(b & 0xFF); - } - return result.toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Native.java b/driver-core/src/main/java/com/datastax/driver/core/Native.java deleted file mode 100644 index 9a28cb2ed6e..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Native.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.lang.reflect.Method; -import jnr.ffi.LibraryLoader; -import jnr.ffi.Pointer; -import jnr.ffi.Runtime; -import jnr.ffi.Struct; -import jnr.ffi.annotations.Out; -import jnr.ffi.annotations.Transient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Helper class to deal with native system calls through the JNR library. - * - *

The driver can benefit from native system calls to improve its performance and accuracy in - * some situations. - * - *

Currently, the following features may be used by the driver when available: - * - *

    - *
  1. {@link #currentTimeMicros()}: thanks to a system call to {@code gettimeofday()}, the driver - * is able to generate timestamps with true microsecond precision (see {@link - * AtomicMonotonicTimestampGenerator} or {@link ThreadLocalMonotonicTimestampGenerator} for - * more information); - *
  2. {@link #processId()}: thanks to a system call to {@code getpid()}, the driver has access to - * the JVM's process ID it is running under – which makes time-based UUID generation easier - * and more reliable (see {@link com.datastax.driver.core.utils.UUIDs UUIDs} for more - * information). - *
- * - *

The availability of the aforementioned system calls depends on the underlying operation - * system's capabilities. For instance, {@code gettimeofday()} is not available under Windows - * systems. You can check if any of the system calls exposed through this class is available by - * calling {@link #isGettimeofdayAvailable()} or {@link #isGetpidAvailable()}. - * - *

Note: This class is public because it needs to be accessible from other packages of the Java - * driver, but it is not meant to be used directly by client code. - * - * @see JNR library on Github - */ -public final class Native { - - private static final Logger LOGGER = LoggerFactory.getLogger(Native.class); - - private static class LibCLoader { - - /** - * Timeval struct. - * - * @see GETTIMEOFDAY(2) - */ - static class Timeval extends Struct { - - public final time_t tv_sec = new time_t(); - - public final Unsigned32 tv_usec = new Unsigned32(); - - public Timeval(Runtime runtime) { - super(runtime); - } - } - - /** Interface for LIBC calls through JNR. Note that this interface must be declared public. */ - public interface LibC { - - /** - * JNR call to {@code gettimeofday}. - * - * @param tv Timeval struct - * @param unused Timezone struct (unused) - * @return 0 for success, or -1 for failure - * @see GETTIMEOFDAY(2) - */ - int gettimeofday(@Out @Transient Timeval tv, Pointer unused); - } - - private static final LibC LIB_C; - - private static final Runtime LIB_C_RUNTIME; - - private static final boolean GETTIMEOFDAY_AVAILABLE; - - static { - LibC libc; - Runtime runtime = null; - try { - libc = LibraryLoader.create(LibC.class).load("c"); - runtime = Runtime.getRuntime(libc); - } catch (Throwable t) { - libc = null; // dereference proxy to library if runtime could not be loaded - if (LOGGER.isDebugEnabled()) - LOGGER.debug( - "Could not load JNR C Library, native system calls through this library will not be available", - t); - else - LOGGER.info( - "Could not load JNR C Library, native system calls through this library will not be available " - + "(set this logger level to DEBUG to see the full stack trace)."); - } - LIB_C = libc; - LIB_C_RUNTIME = runtime; - boolean gettimeofday = false; - if (LIB_C_RUNTIME != null) { - try { - gettimeofday = LIB_C.gettimeofday(new Timeval(LIB_C_RUNTIME), null) == 0; - } catch (Throwable t) { - if (LOGGER.isDebugEnabled()) - LOGGER.debug("Native calls to gettimeofday() not available on this system.", t); - else - LOGGER.info( - "Native calls to gettimeofday() not available on this system " - + "(set this logger level to DEBUG to see the full stack trace)."); - } - } - GETTIMEOFDAY_AVAILABLE = gettimeofday; - } - } - - private static class PosixLoader { - - public static final jnr.posix.POSIX POSIX; - - private static final boolean GETPID_AVAILABLE; - - static { - jnr.posix.POSIX posix; - try { - // use reflection below to get the classloader a chance to load this class - Class posixHandler = Class.forName("jnr.posix.POSIXHandler"); - Class defaultPosixHandler = Class.forName("jnr.posix.util.DefaultPOSIXHandler"); - Class posixFactory = Class.forName("jnr.posix.POSIXFactory"); - Method getPOSIX = posixFactory.getMethod("getPOSIX", posixHandler, Boolean.TYPE); - posix = (jnr.posix.POSIX) getPOSIX.invoke(null, defaultPosixHandler.newInstance(), true); - } catch (Throwable t) { - posix = null; - if (LOGGER.isDebugEnabled()) - LOGGER.debug( - "Could not load JNR POSIX Library, native system calls through this library will not be available.", - t); - else - LOGGER.info( - "Could not load JNR POSIX Library, native system calls through this library will not be available " - + "(set this logger level to DEBUG to see the full stack trace)."); - } - POSIX = posix; - boolean getpid = false; - if (POSIX != null) { - try { - POSIX.getpid(); - getpid = true; - } catch (Throwable t) { - if (LOGGER.isDebugEnabled()) - LOGGER.debug("Native calls to getpid() not available on this system.", t); - else - LOGGER.info( - "Native calls to getpid() not available on this system " - + "(set this logger level to DEBUG to see the full stack trace)."); - } - } - GETPID_AVAILABLE = getpid; - } - } - - /** - * Returns {@code true} if JNR C library is loaded and a call to {@code gettimeofday} is possible - * through this library on this system, and {@code false} otherwise. - * - * @return {@code true} if JNR C library is loaded and a call to {@code gettimeofday} is possible. - */ - public static boolean isGettimeofdayAvailable() { - try { - return LibCLoader.GETTIMEOFDAY_AVAILABLE; - } catch (NoClassDefFoundError e) { - return false; - } - } - - /** - * Returns {@code true} if JNR POSIX library is loaded and a call to {@code getpid} is possible - * through this library on this system, and {@code false} otherwise. - * - * @return {@code true} if JNR POSIX library is loaded and a call to {@code getpid} is possible. - */ - public static boolean isGetpidAvailable() { - try { - return PosixLoader.GETPID_AVAILABLE; - } catch (NoClassDefFoundError e) { - return false; - } - } - - /** - * Returns the current timestamp with microsecond precision via a system call to {@code - * gettimeofday}, through JNR C library. - * - * @return the current timestamp with microsecond precision. - * @throws UnsupportedOperationException if JNR C library is not loaded or {@code gettimeofday} is - * not available. - * @throws IllegalStateException if the call to {@code gettimeofday} did not complete with return - * code 0. - */ - public static long currentTimeMicros() { - if (!isGettimeofdayAvailable()) - throw new UnsupportedOperationException( - "JNR C library not loaded or gettimeofday not available"); - LibCLoader.Timeval tv = new LibCLoader.Timeval(LibCLoader.LIB_C_RUNTIME); - int res = LibCLoader.LIB_C.gettimeofday(tv, null); - if (res != 0) throw new IllegalStateException("Call to gettimeofday failed with result " + res); - return tv.tv_sec.get() * 1000000 + tv.tv_usec.get(); - } - - /** - * Returns the JVM's process identifier (PID) via a system call to {@code getpid}. - * - * @return the JVM's process identifier (PID). - * @throws UnsupportedOperationException if JNR POSIX library is not loaded or {@code getpid} is - * not available. - */ - public static int processId() { - if (!isGetpidAvailable()) - throw new UnsupportedOperationException( - "JNR POSIX library not loaded or getpid not available"); - return PosixLoader.POSIX.getpid(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/NettyOptions.java b/driver-core/src/main/java/com/datastax/driver/core/NettyOptions.java deleted file mode 100644 index d75ed103c9c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/NettyOptions.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.PooledByteBufAllocator; -import io.netty.channel.ChannelOption; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.util.HashedWheelTimer; -import io.netty.util.Timer; -import java.util.concurrent.ThreadFactory; - -/** - * A set of hooks that allow clients to customize the driver's underlying Netty layer. - * - *

Clients that need to hook into the driver's underlying Netty layer can subclass this class and - * provide the necessary customization by overriding its methods. - * - *

Typically, clients would register this class with {@link Cluster#builder()}: - * - *

- * - *

- *     NettyOptions nettyOptions = ...
- *     Cluster cluster = Cluster.builder()
- *          .addContactPoint(...)
- *          .withNettyOptions(nettyOptions)
- *          .build();
- * 
- * - *

Extending the NettyOptions API - * - *

Contrary to other driver options, the options available in this class should be considered as - * advanced features and as such, they should only be modified by expert users. - * - *

A misconfiguration introduced by the means of this API can have unexpected results and - * cause the driver to completely fail to connect. - * - *

Moreover, since versions 2.0.9 and 2.1.4 (see JAVA-538), the driver is available in two - * different flavors: with a standard Maven dependency on Netty, or with a "shaded" (internalized) - * Netty dependency. - * - *

Given that NettyOptions API exposes Netty classes ({@link SocketChannel}, etc.), it should - * only be extended by clients using the non-shaded version of driver. - * - *

Extending this API with shaded Netty classes is not supported, and in particular for - * OSGi applications, it is likely that such a configuration would lead to compile and/or runtime - * errors. - * - * @since 2.0.10 - */ -public class NettyOptions { - - /** The default instance of {@link NettyOptions} to use. */ - public static final NettyOptions DEFAULT_INSTANCE = new NettyOptions(); - - /** - * Return the {@code EventLoopGroup} instance to use. - * - *

This hook is invoked only once at {@link Cluster} initialization; the returned instance will - * be kept in use throughout the cluster lifecycle. - * - *

Typically, implementors would return a newly-created instance; it is however possible to - * re-use a shared instance, but in this case implementors should also override {@link - * #onClusterClose(EventLoopGroup)} to prevent the shared instance to be closed when the cluster - * is closed. - * - *

The default implementation returns a new instance of {@code - * io.netty.channel.epoll.EpollEventLoopGroup} if {@link NettyUtil#isEpollAvailable() epoll is - * available}, or {@code io.netty.channel.nio.NioEventLoopGroup} otherwise. - * - * @param threadFactory The {@link ThreadFactory} to use when creating a new {@code - * EventLoopGroup} instance; The driver will provide its own internal thread factory here. It - * is safe to ignore it and use another thread factory. Note however that for optimal - * performance it is recommended to use a factory that returns {@link - * io.netty.util.concurrent.FastThreadLocalThread} instances (such as Netty's {@link - * java.util.concurrent.Executors.DefaultThreadFactory}). - * @return the {@code EventLoopGroup} instance to use. - */ - public EventLoopGroup eventLoopGroup(ThreadFactory threadFactory) { - return NettyUtil.newEventLoopGroupInstance(threadFactory); - } - - /** - * Return the specific {@code SocketChannel} subclass to use. - * - *

This hook is invoked only once at {@link Cluster} initialization; the returned instance will - * then be used each time the driver creates a new {@link Connection} and configures a new - * instance of {@link Bootstrap} for it. - * - *

The default implementation returns {@code io.netty.channel.epoll.EpollSocketChannel} if - * {@link NettyUtil#isEpollAvailable() epoll is available}, or {@code - * io.netty.channel.socket.nio.NioSocketChannel} otherwise. - * - * @return The {@code SocketChannel} subclass to use. - */ - public Class channelClass() { - return NettyUtil.channelClass(); - } - - /** - * Hook invoked each time the driver creates a new {@link Connection} and configures a new - * instance of {@link Bootstrap} for it. - * - *

This hook is guaranteed to be called after the driver has applied all {@link - * SocketOptions}s. - * - *

This is a good place to add extra {@link io.netty.channel.ChannelHandler ChannelOption}s to - * the boostrap; e.g. plug a custom {@link io.netty.buffer.ByteBufAllocator ByteBufAllocator} - * implementation: - * - *

- * - *

-   * ByteBufAllocator myCustomByteBufAllocator = ...
-   *
-   * public void afterBootstrapInitialized(Bootstrap bootstrap) {
-   *     bootstrap.option(ChannelOption.ALLOCATOR, myCustomByteBufAllocator);
-   * }
-   * 
- * - *

Note that the default implementation of this method configures a pooled {@code - * ByteBufAllocator} (Netty 4.0 defaults to unpooled). If you override this method to set - * unrelated options, make sure you call {@code super.afterBootstrapInitialized(bootstrap)}. - * - * @param bootstrap the {@link Bootstrap} being initialized. - */ - public void afterBootstrapInitialized(Bootstrap bootstrap) { - // In Netty 4.1.x, pooled will be the default, so this won't be necessary anymore - bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); - } - - /** - * Hook invoked each time the driver creates a new {@link Connection} and initializes the {@link - * SocketChannel channel}. - * - *

This hook is guaranteed to be called after the driver has registered all its - * internal channel handlers, and applied the configured {@link SSLOptions}, if any. - * - *

This is a good place to add extra {@link io.netty.channel.ChannelHandler ChannelHandler}s to - * the channel's pipeline; e.g. to add a custom SSL handler to the beginning of the handler chain, - * do the following: - * - *

- * - *

-   * ChannelPipeline pipeline = channel.pipeline();
-   * SSLEngine myCustomSSLEngine = ...
-   * SslHandler myCustomSSLHandler = new SslHandler(myCustomSSLEngine);
-   * pipeline.addFirst("ssl", myCustomSSLHandler);
-   * 
- * - *

Note: if you intend to provide your own SSL implementation, do not enable the driver's - * built-in {@link SSLOptions} at the same time. - * - * @param channel the {@link SocketChannel} instance, after being initialized by the driver. - * @throws Exception if this methods encounters any errors. - */ - public void afterChannelInitialized(SocketChannel channel) throws Exception { - // noop - } - - /** - * Hook invoked when the cluster is shutting down after a call to {@link Cluster#close()}. - * - *

This is guaranteed to be called only after all connections have been individually closed, - * and their channels closed, and only once per {@link EventLoopGroup} instance. - * - *

This gives the implementor a chance to close the {@link EventLoopGroup} properly, if - * required. - * - *

The default implementation initiates a {@link EventLoopGroup#shutdownGracefully() graceful - * shutdown} of the passed {@link EventLoopGroup}, then waits uninterruptibly for the shutdown to - * complete or timeout. - * - *

Implementation note: if the {@link EventLoopGroup} instance is being shared, or used for - * other purposes than to coordinate Netty events for the current cluster, then it should not be - * shut down here; subclasses would have to override this method accordingly to take the - * appropriate action. - * - * @param eventLoopGroup the event loop group used by the cluster being closed - */ - public void onClusterClose(EventLoopGroup eventLoopGroup) { - eventLoopGroup.shutdownGracefully().syncUninterruptibly(); - } - - /** - * Return the {@link Timer} instance used by Read Timeouts and Speculative Execution. - * - *

This hook is invoked only once at {@link Cluster} initialization; the returned instance will - * be kept in use throughout the cluster lifecycle. - * - *

Typically, implementors would return a newly-created instance; it is however possible to - * re-use a shared instance, but in this case implementors should also override {@link - * #onClusterClose(Timer)} to prevent the shared instance to be closed when the cluster is closed. - * - *

The default implementation returns a new instance created by {@link - * HashedWheelTimer#HashedWheelTimer(ThreadFactory)}. - * - * @param threadFactory The {@link ThreadFactory} to use when creating a new {@link - * HashedWheelTimer} instance; The driver will provide its own internal thread factory here. - * It is safe to ignore it and use another thread factory. Note however that for optimal - * performance it is recommended to use a factory that returns {@link - * io.netty.util.concurrent.FastThreadLocalThread} instances (such as Netty's {@link - * java.util.concurrent.Executors.DefaultThreadFactory}). - * @return the {@link Timer} instance to use. - */ - public Timer timer(ThreadFactory threadFactory) { - return new HashedWheelTimer(threadFactory); - } - - /** - * Hook invoked when the cluster is shutting down after a call to {@link Cluster#close()}. - * - *

This is guaranteed to be called only after all connections have been individually closed, - * and their channels closed, and only once per {@link Timer} instance. - * - *

This gives the implementor a chance to close the {@link Timer} properly, if required. - * - *

The default implementation calls a {@link Timer#stop()} of the passed {@link Timer} - * instance. - * - *

Implementation note: if the {@link Timer} instance is being shared, or used for other - * purposes than to schedule actions for the current cluster, than it should not be stopped here; - * subclasses would have to override this method accordingly to take the appropriate action. - * - * @param timer the timer used by the cluster being closed - */ - public void onClusterClose(Timer timer) { - timer.stop(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/NettySSLOptions.java b/driver-core/src/main/java/com/datastax/driver/core/NettySSLOptions.java deleted file mode 100644 index e1bf1217a37..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/NettySSLOptions.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslHandler; - -/** - * {@link SSLOptions} implementation based on Netty's SSL context. - * - *

Netty has the ability to use OpenSSL if available, instead of the JDK's built-in engine. This - * yields better performance. - * - * @deprecated Use {@link RemoteEndpointAwareNettySSLOptions} instead. - */ -@SuppressWarnings("DeprecatedIsStillUsed") -@Deprecated -public class NettySSLOptions implements SSLOptions { - protected final SslContext context; - - /** - * Create a new instance from a given context. - * - * @param context the Netty context. {@code SslContextBuilder.forClient()} provides a fluent API - * to build it. - */ - public NettySSLOptions(SslContext context) { - this.context = context; - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel) { - return context.newHandler(channel.alloc()); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/NettyUtil.java b/driver-core/src/main/java/com/datastax/driver/core/NettyUtil.java deleted file mode 100644 index c551be5f6b6..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/NettyUtil.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.base.Throwables; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioSocketChannel; -import java.lang.reflect.Constructor; -import java.util.Locale; -import java.util.concurrent.ThreadFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** A set of utilities related to the underlying Netty layer. */ -@SuppressWarnings("unchecked") -class NettyUtil { - - private static final boolean FORCE_NIO = - SystemProperties.getBoolean("com.datastax.driver.FORCE_NIO", false); - - private static final Logger LOGGER = LoggerFactory.getLogger(NettyUtil.class); - - private static final boolean USE_EPOLL; - - private static final Constructor EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR; - - private static final Class EPOLL_CHANNEL_CLASS; - - private static final Class[] EVENT_GROUP_ARGUMENTS = {int.class, ThreadFactory.class}; - - private static final String SHADING_DETECTION_STRING = - "io.netty.shadingdetection.ShadingDetection"; - - private static final boolean SHADED = - !SHADING_DETECTION_STRING.equals( - String.format("%s.%s.shadingdetection.ShadingDetection", "io", "netty")); - - static { - boolean useEpoll = false; - if (!SHADED) { - try { - Class epoll = Class.forName("io.netty.channel.epoll.Epoll"); - if (FORCE_NIO) { - LOGGER.info( - "Found Netty's native epoll transport in the classpath, " - + "but NIO was forced through the FORCE_NIO system property."); - } else if (!System.getProperty("os.name", "").toLowerCase(Locale.US).equals("linux")) { - LOGGER.warn( - "Found Netty's native epoll transport, but not running on linux-based operating " - + "system. Using NIO instead."); - } else if (!(Boolean) epoll.getMethod("isAvailable").invoke(null)) { - LOGGER.warn( - "Found Netty's native epoll transport in the classpath, but epoll is not available. " - + "Using NIO instead.", - (Throwable) epoll.getMethod("unavailabilityCause").invoke(null)); - } else { - LOGGER.info("Found Netty's native epoll transport in the classpath, using it"); - useEpoll = true; - } - } catch (ClassNotFoundException e) { - LOGGER.info( - "Did not find Netty's native epoll transport in the classpath, defaulting to NIO."); - } catch (Exception e) { - LOGGER.warn( - "Unexpected error trying to find Netty's native epoll transport in the classpath, defaulting to NIO.", - e); - } - } else { - LOGGER.info( - "Detected shaded Netty classes in the classpath; native epoll transport will not work properly, " - + "defaulting to NIO."); - } - USE_EPOLL = useEpoll; - Constructor constructor = null; - Class channelClass = null; - if (USE_EPOLL) { - try { - channelClass = - (Class) - Class.forName("io.netty.channel.epoll.EpollSocketChannel"); - Class epoolEventLoupGroupClass = - Class.forName("io.netty.channel.epoll.EpollEventLoopGroup"); - constructor = - (Constructor) - epoolEventLoupGroupClass.getDeclaredConstructor(EVENT_GROUP_ARGUMENTS); - } catch (Exception e) { - throw new AssertionError( - "Netty's native epoll is in use but cannot locate Epoll classes, this should not happen: " - + e); - } - } - EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR = constructor; - EPOLL_CHANNEL_CLASS = channelClass; - } - - /** @return true if the current driver bundle is using shaded Netty classes, false otherwise. */ - public static boolean isShaded() { - return SHADED; - } - - /** @return true if native epoll transport is available in the classpath, false otherwise. */ - public static boolean isEpollAvailable() { - return USE_EPOLL; - } - - /** - * Return a new instance of {@link EventLoopGroup}. - * - *

Returns an instance of {@link io.netty.channel.epoll.EpollEventLoopGroup} if {@link - * #isEpollAvailable() epoll is available}, or an instance of {@link NioEventLoopGroup} otherwise. - * - * @param factory the {@link ThreadFactory} instance to use to create the new instance of {@link - * EventLoopGroup} - * @return a new instance of {@link EventLoopGroup} - */ - public static EventLoopGroup newEventLoopGroupInstance(ThreadFactory factory) { - if (isEpollAvailable()) { - try { - return EPOLL_EVENT_LOOP_GROUP_CONSTRUCTOR.newInstance(0, factory); - } catch (Exception e) { - throw Throwables.propagate(e); // should not happen - } - } else { - return new NioEventLoopGroup(0, factory); - } - } - - /** - * Return the SocketChannel class to use. - * - *

Returns an instance of {@link io.netty.channel.epoll.EpollSocketChannel} if {@link - * #isEpollAvailable() epoll is available}, or an instance of {@link NioSocketChannel} otherwise. - * - * @return the SocketChannel class to use. - */ - public static Class channelClass() { - if (isEpollAvailable()) { - return EPOLL_CHANNEL_CLASS; - } else { - return NioSocketChannel.class; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/OutboundTrafficMeter.java b/driver-core/src/main/java/com/datastax/driver/core/OutboundTrafficMeter.java deleted file mode 100644 index 6855d0cac7a..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/OutboundTrafficMeter.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.codahale.metrics.Meter; -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelOutboundHandlerAdapter; -import io.netty.channel.ChannelPromise; - -@Sharable -class OutboundTrafficMeter extends ChannelOutboundHandlerAdapter { - - private final Meter meter; - - OutboundTrafficMeter(Meter meter) { - this.meter = meter; - } - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) - throws Exception { - if (msg instanceof ByteBuf) { - meter.mark(((ByteBuf) msg).readableBytes()); - } - super.write(ctx, msg, promise); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/PagingIterable.java b/driver-core/src/main/java/com/datastax/driver/core/PagingIterable.java deleted file mode 100644 index 9e5af114dbf..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/PagingIterable.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.util.concurrent.ListenableFuture; -import java.util.Iterator; -import java.util.List; - -/** - * Defines an iterable whose elements can be remotely fetched and paged, possibly asynchronously. - */ -public interface PagingIterable, T> extends Iterable { - - /** - * Returns whether this result set has more results. - * - * @return whether this result set has more results. - */ - boolean isExhausted(); - - /** - * Whether all results from this result set have been fetched from the database. - * - *

Note that if {@code isFullyFetched()}, then {@link #getAvailableWithoutFetching} will return - * how many rows remain in the result set before exhaustion. But please note that {@code - * isFullyFetched()} never guarantees that the result set is exhausted (you should call {@link - * #isExhausted()} to verify it). - * - * @return whether all results have been fetched. - */ - boolean isFullyFetched(); - - /** - * The number of rows that can be retrieved from this result set without blocking to fetch. - * - * @return the number of rows readily available in this result set. If {@link #isFullyFetched()}, - * this is the total number of rows remaining in this result set (after which the result set - * will be exhausted). - */ - int getAvailableWithoutFetching(); - - /** - * Force fetching the next page of results for this result set, if any. - * - *

This method is entirely optional. It will be called automatically while the result set is - * consumed (through {@link #one}, {@link #all} or iteration) when needed (i.e. when {@code - * getAvailableWithoutFetching() == 0} and {@code isFullyFetched() == false}). - * - *

You can however call this method manually to force the fetching of the next page of results. - * This can allow to prefetch results before they are strictly needed. For instance, if you want - * to prefetch the next page of results as soon as there is less than 100 rows readily available - * in this result set, you can do: - * - *

-   *   ResultSet rs = session.execute(...);
-   *   Iterator<Row> iter = rs.iterator();
-   *   while (iter.hasNext()) {
-   *       if (rs.getAvailableWithoutFetching() == 100 && !rs.isFullyFetched())
-   *           rs.fetchMoreResults();
-   *       Row row = iter.next()
-   *       ... process the row ...
-   *   }
-   * 
- * - * This method is not blocking, so in the example above, the call to {@code fetchMoreResults} will - * not block the processing of the 100 currently available rows (but {@code iter.hasNext()} will - * block once those rows have been processed until the fetch query returns, if it hasn't yet). - * - *

Only one page of results (for a given result set) can be fetched at any given time. If this - * method is called twice and the query triggered by the first call has not returned yet when the - * second one is performed, then the 2nd call will simply return a future on the currently in - * progress query. - * - * @return a future on the completion of fetching the next page of results. If the result set is - * already fully retrieved ({@code isFullyFetched() == true}), then the returned future will - * return immediately but not particular error will be thrown (you should thus call {@link - * #isFullyFetched()} to know if calling this method can be of any use}). - */ - ListenableFuture fetchMoreResults(); - - /** - * Returns the next result from this result set. - * - * @return the next row in this result set or null if this result set is exhausted. - */ - T one(); - - /** - * Returns all the remaining rows in this result set as a list. - * - *

Note that, contrary to {@link #iterator()} or successive calls to {@link #one()}, this - * method forces fetching the full content of the result set at once, holding it all in memory in - * particular. It is thus recommended to prefer iterations through {@link #iterator()} when - * possible, especially if the result set can be big. - * - * @return a list containing the remaining results of this result set. The returned list is empty - * if and only the result set is exhausted. The result set will be exhausted after a call to - * this method. - */ - List all(); - - /** - * Returns an iterator over the rows contained in this result set. - * - *

The {@link Iterator#next} method is equivalent to calling {@link #one}. So this iterator - * will consume results from this result set and after a full iteration, the result set will be - * empty. - * - *

The returned iterator does not support the {@link Iterator#remove} method. - * - * @return an iterator that will consume and return the remaining rows of this result set. - */ - Iterator iterator(); - - /** - * Returns information on the execution of the last query made for this result set. - * - *

Note that in most cases, a result set is fetched with only one query, but large result sets - * can be paged and thus be retrieved by multiple queries. In that case this method return the - * {@link ExecutionInfo} for the last query performed. To retrieve the information for all - * queries, use {@link #getAllExecutionInfo}. - * - *

The returned object includes basic information such as the queried hosts, but also the - * Cassandra query trace if tracing was enabled for the query. - * - * @return the execution info for the last query made for this result set. - */ - ExecutionInfo getExecutionInfo(); - - /** - * Return the execution information for all queries made to retrieve this result set. - * - *

Unless the result set is large enough to get paged underneath, the returned list will be - * singleton. If paging has been used however, the returned list contains the {@link - * ExecutionInfo} objects for all the queries done to obtain this result set (at the time of the - * call) in the order those queries were made. - * - * @return a list of the execution info for all the queries made for this result set. - */ - List getAllExecutionInfo(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/PagingState.java b/driver-core/src/main/java/com/datastax/driver/core/PagingState.java deleted file mode 100644 index 477f72131c6..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/PagingState.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.PagingStateException; -import com.datastax.driver.core.utils.Bytes; -import java.nio.ByteBuffer; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -/** - * The paging state of a query. - * - *

This object represents the next page to be fetched if the query is multi page. It can be saved - * and reused later on the same statement. - * - *

The PagingState can be serialized and deserialized either as a String or as a byte array. - * - * @see Statement#setPagingState(PagingState) - */ -public class PagingState { - - private final byte[] pagingState; - private final byte[] hash; - private final ProtocolVersion protocolVersion; - - PagingState( - ByteBuffer pagingState, - Statement statement, - ProtocolVersion protocolVersion, - CodecRegistry codecRegistry) { - this.pagingState = Bytes.getArray(pagingState); - this.hash = hash(statement, protocolVersion, codecRegistry); - this.protocolVersion = protocolVersion; - } - - // The serialized form of the paging state is: - // size of raw state|size of hash|raw state|hash|protocol version - // - // The protocol version might be absent, in which case it defaults to V2 (this is for backward - // compatibility with 2.0.10 where it is always absent). - private PagingState(byte[] complete) { - // Check the sizes in the beginning of the buffer, otherwise we cannot build the paging state - // object - ByteBuffer pagingStateBB = ByteBuffer.wrap(complete); - int pagingSize = pagingStateBB.getShort(); - int hashSize = pagingStateBB.getShort(); - if (pagingSize + hashSize != pagingStateBB.remaining() - && pagingSize + hashSize + 2 != pagingStateBB.remaining()) { - throw new PagingStateException( - "Cannot deserialize paging state, invalid format. " - + "The serialized form was corrupted, or not initially generated from a PagingState object."); - } - this.pagingState = new byte[pagingSize]; - pagingStateBB.get(this.pagingState); - this.hash = new byte[hashSize]; - pagingStateBB.get(this.hash); - this.protocolVersion = - (pagingStateBB.remaining() > 0) - ? ProtocolVersion.fromInt(pagingStateBB.getShort()) - : ProtocolVersion.V2; - } - - private byte[] hash( - Statement statement, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - byte[] digest; - ByteBuffer[] values; - MessageDigest md; - if (statement instanceof StatementWrapper) { - statement = ((StatementWrapper) statement).getWrappedStatement(); - } - assert !(statement instanceof BatchStatement); - try { - md = MessageDigest.getInstance("MD5"); - if (statement instanceof BoundStatement) { - BoundStatement bs = ((BoundStatement) statement); - md.update(bs.preparedStatement().getQueryString().getBytes()); - values = bs.wrapper.values; - } else { - // it is a RegularStatement since Batch statements are not allowed - RegularStatement rs = (RegularStatement) statement; - md.update(rs.getQueryString().getBytes()); - values = rs.getValues(protocolVersion, codecRegistry); - } - if (values != null) { - for (ByteBuffer value : values) { - md.update(value.duplicate()); - } - } - md.update(this.pagingState); - digest = md.digest(); - - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("MD5 doesn't seem to be available on this JVM", e); - } - return digest; - } - - boolean matches(Statement statement, CodecRegistry codecRegistry) { - byte[] toTest = hash(statement, protocolVersion, codecRegistry); - return Arrays.equals(toTest, this.hash); - } - - private ByteBuffer generateCompleteOutput() { - ByteBuffer res = ByteBuffer.allocate(pagingState.length + hash.length + 6); - - res.putShort((short) pagingState.length); - res.putShort((short) hash.length); - - res.put(pagingState); - res.put(hash); - - res.putShort((short) protocolVersion.toInt()); - - res.rewind(); - - return res; - } - - ByteBuffer getRawState() { - return ByteBuffer.wrap(this.pagingState); - } - - @Override - public String toString() { - return Bytes.toRawHexString(generateCompleteOutput()); - } - - /** - * Create a PagingState object from a string previously generated with {@link #toString()}. - * - * @param string the string value. - * @return the PagingState object created. - * @throws PagingStateException if the string does not have the correct format. - */ - public static PagingState fromString(String string) { - try { - byte[] complete = Bytes.fromRawHexString(string, 0); - return new PagingState(complete); - } catch (Exception e) { - throw new PagingStateException( - "Cannot deserialize paging state, invalid format. " - + "The serialized form was corrupted, or not initially generated from a PagingState object.", - e); - } - } - - /** - * Return a representation of the paging state object as a byte array. - * - * @return the paging state as a byte array. - */ - public byte[] toBytes() { - return generateCompleteOutput().array(); - } - - /** - * Create a PagingState object from a byte array previously generated with {@link #toBytes()}. - * - * @param pagingState The byte array representation. - * @return the PagingState object created. - * @throws PagingStateException if the byte array does not have the correct format. - */ - public static PagingState fromBytes(byte[] pagingState) { - return new PagingState(pagingState); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ParseUtils.java b/driver-core/src/main/java/com/datastax/driver/core/ParseUtils.java deleted file mode 100644 index eff44740038..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ParseUtils.java +++ /dev/null @@ -1,564 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.text.ParseException; -import java.text.ParsePosition; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; -import java.util.concurrent.TimeUnit; - -/** Simple utility class used to help parsing CQL values (mainly UDT and collection ones). */ -public abstract class ParseUtils { - - /** Valid ISO-8601 patterns for CQL timestamp literals. */ - private static final String[] iso8601Patterns = - new String[] { - "yyyy-MM-dd HH:mm", - "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd HH:mmZ", - "yyyy-MM-dd HH:mm:ssZ", - "yyyy-MM-dd HH:mm:ss.SSS", - "yyyy-MM-dd HH:mm:ss.SSSZ", - "yyyy-MM-dd'T'HH:mm", - "yyyy-MM-dd'T'HH:mmZ", - "yyyy-MM-dd'T'HH:mm:ss", - "yyyy-MM-dd'T'HH:mm:ssZ", - "yyyy-MM-dd'T'HH:mm:ss.SSS", - "yyyy-MM-dd'T'HH:mm:ss.SSSZ", - "yyyy-MM-dd", - "yyyy-MM-ddZ" - }; - - /** - * Returns the index of the first character in toParse from idx that is not a "space". - * - * @param toParse the string to skip space on. - * @param idx the index to start skipping space from. - * @return the index of the first character in toParse from idx that is not a "space. - */ - public static int skipSpaces(String toParse, int idx) { - while (isBlank(toParse.charAt(idx)) && idx < toParse.length()) ++idx; - return idx; - } - - /** - * Assuming that idx points to the beginning of a CQL value in toParse, returns the index of the - * first character after this value. - * - * @param toParse the string to skip a value form. - * @param idx the index to start parsing a value from. - * @return the index ending the CQL value starting at {@code idx}. - * @throws IllegalArgumentException if idx doesn't point to the start of a valid CQL value. - */ - public static int skipCQLValue(String toParse, int idx) { - if (idx >= toParse.length()) throw new IllegalArgumentException(); - - if (isBlank(toParse.charAt(idx))) throw new IllegalArgumentException(); - - int cbrackets = 0; - int sbrackets = 0; - int parens = 0; - boolean inString = false; - - do { - char c = toParse.charAt(idx); - if (inString) { - if (c == '\'') { - if (idx + 1 < toParse.length() && toParse.charAt(idx + 1) == '\'') { - ++idx; // this is an escaped quote, skip it - } else { - inString = false; - if (cbrackets == 0 && sbrackets == 0 && parens == 0) return idx + 1; - } - } - // Skip any other character - } else if (c == '\'') { - inString = true; - } else if (c == '{') { - ++cbrackets; - } else if (c == '[') { - ++sbrackets; - } else if (c == '(') { - ++parens; - } else if (c == '}') { - if (cbrackets == 0) return idx; - - --cbrackets; - if (cbrackets == 0 && sbrackets == 0 && parens == 0) return idx + 1; - } else if (c == ']') { - if (sbrackets == 0) return idx; - - --sbrackets; - if (cbrackets == 0 && sbrackets == 0 && parens == 0) return idx + 1; - } else if (c == ')') { - if (parens == 0) return idx; - - --parens; - if (cbrackets == 0 && sbrackets == 0 && parens == 0) return idx + 1; - } else if (isBlank(c) || !isIdentifierChar(c)) { - if (cbrackets == 0 && sbrackets == 0 && parens == 0) return idx; - } - } while (++idx < toParse.length()); - - if (inString || cbrackets != 0 || sbrackets != 0 || parens != 0) - throw new IllegalArgumentException(); - return idx; - } - - /** - * Assuming that idx points to the beginning of a CQL identifier in toParse, returns the index of - * the first character after this identifier. - * - * @param toParse the string to skip an identifier from. - * @param idx the index to start parsing an identifier from. - * @return the index ending the CQL identifier starting at {@code idx}. - * @throws IllegalArgumentException if idx doesn't point to the start of a valid CQL identifier. - */ - public static int skipCQLId(String toParse, int idx) { - if (idx >= toParse.length()) throw new IllegalArgumentException(); - - char c = toParse.charAt(idx); - if (isIdentifierChar(c)) { - while (idx < toParse.length() && isIdentifierChar(toParse.charAt(idx))) idx++; - return idx; - } - - if (c != '"') throw new IllegalArgumentException(); - - while (++idx < toParse.length()) { - c = toParse.charAt(idx); - if (c != '"') continue; - - if (idx + 1 < toParse.length() && toParse.charAt(idx + 1) == '\"') - ++idx; // this is an escaped double quote, skip it - else return idx + 1; - } - throw new IllegalArgumentException(); - } - - /** - * Return {@code true} if the given character is allowed in a CQL identifier, that is, if it is in - * the range: {@code [0..9a..zA..Z-+._&]}. - * - * @param c The character to inspect. - * @return {@code true} if the given character is allowed in a CQL identifier, {@code false} - * otherwise. - */ - public static boolean isIdentifierChar(int c) { - return (c >= '0' && c <= '9') - || (c >= 'a' && c <= 'z') - || (c >= 'A' && c <= 'Z') - || c == '-' - || c == '+' - || c == '.' - || c == '_' - || c == '&'; - } - - /** - * Return {@code true} if the given character is a valid whitespace character in CQL, that is, if - * it is a regular space, a tabulation sign, or a new line sign. - * - * @param c The character to inspect. - * @return {@code true} if the given character is a valid whitespace character, {@code false} - * otherwise. - */ - public static boolean isBlank(int c) { - return c == ' ' || c == '\t' || c == '\n'; - } - - /** - * Check whether the given string corresponds to a valid CQL long literal. Long literals are - * composed solely by digits, but can have an optional leading minus sign. - * - * @param str The string to inspect. - * @return {@code true} if the given string corresponds to a valid CQL integer literal, {@code - * false} otherwise. - */ - public static boolean isLongLiteral(String str) { - if (str == null || str.isEmpty()) return false; - char[] chars = str.toCharArray(); - for (int i = 0; i < chars.length; i++) { - char c = chars[i]; - if ((c < '0' && (i != 0 || c != '-')) || c > '9') return false; - } - return true; - } - - /** - * Return {@code true} if the given string is surrounded by single quotes, and {@code false} - * otherwise. - * - * @param value The string to inspect. - * @return {@code true} if the given string is surrounded by single quotes, and {@code false} - * otherwise. - */ - public static boolean isQuoted(String value) { - return isQuoted(value, '\''); - } - - /** - * Quote the given string; single quotes are escaped. If the given string is null, this method - * returns a quoted empty string ({@code ''}). - * - * @param value The value to quote. - * @return The quoted string. - */ - public static String quote(String value) { - return quote(value, '\''); - } - - /** - * Unquote the given string if it is quoted; single quotes are unescaped. If the given string is - * not quoted, it is returned without any modification. - * - * @param value The string to unquote. - * @return The unquoted string. - */ - public static String unquote(String value) { - return unquote(value, '\''); - } - - /** - * Return {@code true} if the given string is surrounded by double quotes, and {@code false} - * otherwise. - * - * @param value The string to inspect. - * @return {@code true} if the given string is surrounded by double quotes, and {@code false} - * otherwise. - */ - public static boolean isDoubleQuoted(String value) { - return isQuoted(value, '\"'); - } - - /** - * Double quote the given string; double quotes are escaped. If the given string is null, this - * method returns a quoted empty string ({@code ""}). - * - * @param value The value to double quote. - * @return The double quoted string. - */ - public static String doubleQuote(String value) { - return quote(value, '"'); - } - - /** - * Unquote the given string if it is double quoted; double quotes are unescaped. If the given - * string is not double quoted, it is returned without any modification. - * - * @param value The string to un-double quote. - * @return The un-double quoted string. - */ - public static String unDoubleQuote(String value) { - return unquote(value, '"'); - } - - /** - * Parse the given string as a date, using one of the accepted ISO-8601 date patterns. - * - *

This method is adapted from Apache Commons {@code DateUtils.parseStrictly()} method (that is - * used Cassandra side to parse date strings).. - * - * @throws ParseException If the given string is not a valid ISO-8601 date. - * @see 'Working with - * timestamps' section of CQL specification - */ - public static Date parseDate(String str) throws ParseException { - SimpleDateFormat parser = new SimpleDateFormat(); - parser.setLenient(false); - // set a default timezone for patterns that do not provide one - parser.setTimeZone(TimeZone.getTimeZone("UTC")); - // Java 6 has very limited support for ISO-8601 time zone formats, - // so we need to transform the string first - // so that accepted patterns are correctly handled, - // such as Z for UTC, or "+00:00" instead of "+0000". - // Note: we cannot use the X letter in the pattern - // because it has been introduced in Java 7. - str = str.replaceAll("(\\+|\\-)(\\d\\d):(\\d\\d)$", "$1$2$3"); - str = str.replaceAll("Z$", "+0000"); - ParsePosition pos = new ParsePosition(0); - for (String parsePattern : iso8601Patterns) { - parser.applyPattern(parsePattern); - pos.setIndex(0); - Date date = parser.parse(str, pos); - if (date != null && pos.getIndex() == str.length()) { - return date; - } - } - throw new ParseException("Unable to parse the date: " + str, -1); - } - - /** - * Parse the given string as a date, using the supplied date pattern. - * - *

This method is adapted from Apache Commons {@code DateUtils.parseStrictly()} method (that is - * used Cassandra side to parse date strings).. - * - * @throws ParseException If the given string cannot be parsed with the given pattern. - * @see 'Working with - * timestamps' section of CQL specification - */ - public static Date parseDate(String str, String pattern) throws ParseException { - SimpleDateFormat parser = new SimpleDateFormat(); - parser.setLenient(false); - // set a default timezone for patterns that do not provide one - parser.setTimeZone(TimeZone.getTimeZone("UTC")); - // Java 6 has very limited support for ISO-8601 time zone formats, - // so we need to transform the string first - // so that accepted patterns are correctly handled, - // such as Z for UTC, or "+00:00" instead of "+0000". - // Note: we cannot use the X letter in the pattern - // because it has been introduced in Java 7. - str = str.replaceAll("(\\+|\\-)(\\d\\d):(\\d\\d)$", "$1$2$3"); - str = str.replaceAll("Z$", "+0000"); - ParsePosition pos = new ParsePosition(0); - parser.applyPattern(pattern); - pos.setIndex(0); - Date date = parser.parse(str, pos); - if (date != null && pos.getIndex() == str.length()) { - return date; - } - throw new ParseException("Unable to parse the date: " + str, -1); - } - - /** - * Parse the given string as a time, using the following time pattern: {@code - * hh:mm:ss[.fffffffff]}. - * - *

This method is loosely based on {@code java.sql.Timestamp}. - * - * @param str The string to parse. - * @return A long value representing the number of nanoseconds since midnight. - * @throws ParseException if the string cannot be parsed. - * @see 'Working with time' - * section of CQL specification - */ - public static long parseTime(String str) throws ParseException { - String nanos_s; - - long hour; - long minute; - long second; - long a_nanos = 0; - - String formatError = "Timestamp format must be hh:mm:ss[.fffffffff]"; - String zeros = "000000000"; - - if (str == null) throw new IllegalArgumentException(formatError); - str = str.trim(); - - // Parse the time - int firstColon = str.indexOf(':'); - int secondColon = str.indexOf(':', firstColon + 1); - - // Convert the time; default missing nanos - if (firstColon > 0 && secondColon > 0 && secondColon < str.length() - 1) { - int period = str.indexOf('.', secondColon + 1); - hour = Integer.parseInt(str.substring(0, firstColon)); - if (hour < 0 || hour >= 24) throw new IllegalArgumentException("Hour out of bounds."); - - minute = Integer.parseInt(str.substring(firstColon + 1, secondColon)); - if (minute < 0 || minute >= 60) throw new IllegalArgumentException("Minute out of bounds."); - - if (period > 0 && period < str.length() - 1) { - second = Integer.parseInt(str.substring(secondColon + 1, period)); - if (second < 0 || second >= 60) throw new IllegalArgumentException("Second out of bounds."); - - nanos_s = str.substring(period + 1); - if (nanos_s.length() > 9) throw new IllegalArgumentException(formatError); - if (!Character.isDigit(nanos_s.charAt(0))) throw new IllegalArgumentException(formatError); - nanos_s = nanos_s + zeros.substring(0, 9 - nanos_s.length()); - a_nanos = Integer.parseInt(nanos_s); - } else if (period > 0) throw new ParseException(formatError, -1); - else { - second = Integer.parseInt(str.substring(secondColon + 1)); - if (second < 0 || second >= 60) throw new ParseException("Second out of bounds.", -1); - } - } else throw new ParseException(formatError, -1); - - long rawTime = 0; - rawTime += TimeUnit.HOURS.toNanos(hour); - rawTime += TimeUnit.MINUTES.toNanos(minute); - rawTime += TimeUnit.SECONDS.toNanos(second); - rawTime += a_nanos; - return rawTime; - } - - /** - * Format the given long value as a CQL time literal, using the following time pattern: {@code - * hh:mm:ss[.fffffffff]}. - * - * @param value A long value representing the number of nanoseconds since midnight. - * @return The formatted value. - * @see 'Working with time' - * section of CQL specification - */ - public static String formatTime(long value) { - int nano = (int) (value % 1000000000); - value -= nano; - value /= 1000000000; - int seconds = (int) (value % 60); - value -= seconds; - value /= 60; - int minutes = (int) (value % 60); - value -= minutes; - value /= 60; - int hours = (int) (value % 24); - value -= hours; - value /= 24; - assert (value == 0); - StringBuilder sb = new StringBuilder(); - leftPadZeros(hours, 2, sb); - sb.append(":"); - leftPadZeros(minutes, 2, sb); - sb.append(":"); - leftPadZeros(seconds, 2, sb); - sb.append("."); - leftPadZeros(nano, 9, sb); - return sb.toString(); - } - - /** - * Return {@code true} if the given string is surrounded by the quote character given, and {@code - * false} otherwise. - * - * @param value The string to inspect. - * @return {@code true} if the given string is surrounded by the quote character, and {@code - * false} otherwise. - */ - private static boolean isQuoted(String value, char quoteChar) { - return value != null - && value.length() > 1 - && value.charAt(0) == quoteChar - && value.charAt(value.length() - 1) == quoteChar; - } - - /** - * @param quoteChar " or ' - * @return A quoted empty string. - */ - private static String emptyQuoted(char quoteChar) { - // don't handle non quote characters, this is done so that these are interned and don't create - // repeated empty quoted strings. - assert quoteChar == '"' || quoteChar == '\''; - if (quoteChar == '"') return "\"\""; - else return "''"; - } - - /** - * Quotes text and escapes any existing quotes in the text. {@code String.replace()} is a bit too - * inefficient (see JAVA-67, JAVA-1262). - * - * @param text The text. - * @param quoteChar The character to use as a quote. - * @return The text with surrounded in quotes with all existing quotes escaped with (i.e. ' - * becomes '') - */ - private static String quote(String text, char quoteChar) { - if (text == null || text.isEmpty()) return emptyQuoted(quoteChar); - - int nbMatch = 0; - int start = -1; - do { - start = text.indexOf(quoteChar, start + 1); - if (start != -1) ++nbMatch; - } while (start != -1); - - // no quotes found that need to be escaped, simply surround in quotes and return. - if (nbMatch == 0) return quoteChar + text + quoteChar; - - // 2 for beginning and end quotes. - // length for original text - // nbMatch for escape characters to add to quotes to be escaped. - int newLength = 2 + text.length() + nbMatch; - char[] result = new char[newLength]; - result[0] = quoteChar; - result[newLength - 1] = quoteChar; - int newIdx = 1; - for (int i = 0; i < text.length(); i++) { - char c = text.charAt(i); - if (c == quoteChar) { - // escape quote with another occurrence. - result[newIdx++] = c; - result[newIdx++] = c; - } else { - result[newIdx++] = c; - } - } - return new String(result); - } - - /** - * Unquotes text and unescapes non surrounding quotes. {@code String.replace()} is a bit too - * inefficient (see JAVA-67, JAVA-1262). - * - * @param text The text - * @param quoteChar The character to use as a quote. - * @return The text with surrounding quotes removed and non surrounding quotes unescaped (i.e. '' - * becomes ') - */ - private static String unquote(String text, char quoteChar) { - if (!isQuoted(text, quoteChar)) return text; - - if (text.length() == 2) return ""; - - String search = emptyQuoted(quoteChar); - int nbMatch = 0; - int start = -1; - do { - start = text.indexOf(search, start + 2); - // ignore the second to last character occurrence, as the last character is a quote. - if (start != -1 && start != text.length() - 2) ++nbMatch; - } while (start != -1); - - // no escaped quotes found, simply remove surrounding quotes and return. - if (nbMatch == 0) return text.substring(1, text.length() - 1); - - // length of the new string will be its current length - the number of occurrences. - int newLength = text.length() - nbMatch - 2; - char[] result = new char[newLength]; - int newIdx = 0; - // track whenever a quoteChar is encountered and the previous character is not a quoteChar. - boolean firstFound = false; - for (int i = 1; i < text.length() - 1; i++) { - char c = text.charAt(i); - if (c == quoteChar) { - if (firstFound) { - // The previous character was a quoteChar, don't add this to result, this action in - // effect removes consecutive quotes. - firstFound = false; - } else { - // found a quoteChar and the previous character was not a quoteChar, include in result. - firstFound = true; - result[newIdx++] = c; - } - } else { - // non quoteChar encountered, include in result. - result[newIdx++] = c; - firstFound = false; - } - } - return new String(result); - } - - private static void leftPadZeros(int value, int digits, StringBuilder sb) { - sb.append(String.format("%0" + digits + "d", value)); - } - - private ParseUtils() {} -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/PerHostPercentileTracker.java b/driver-core/src/main/java/com/datastax/driver/core/PerHostPercentileTracker.java deleted file mode 100644 index 828a77698d0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/PerHostPercentileTracker.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * A {@code PercentileTracker} that maintains a separate histogram for each host. - * - *

This gives you per-host latency percentiles, meaning that each host will only be compared to - * itself. - */ -public class PerHostPercentileTracker extends PercentileTracker { - private PerHostPercentileTracker( - long highestTrackableLatencyMillis, - int numberOfSignificantValueDigits, - int minRecordedValues, - long intervalMs) { - super( - highestTrackableLatencyMillis, - numberOfSignificantValueDigits, - minRecordedValues, - intervalMs); - } - - @Override - protected Host computeKey(Host host, Statement statement, Exception exception) { - return host; - } - - /** - * Returns a builder to create a new instance. - * - * @param highestTrackableLatencyMillis the highest expected latency. If a higher value is - * reported, it will be ignored and a warning will be logged. A good rule of thumb is to set - * it slightly higher than {@link SocketOptions#getReadTimeoutMillis()}. - * @return the builder. - */ - public static Builder builder(long highestTrackableLatencyMillis) { - return new Builder(highestTrackableLatencyMillis); - } - - /** Helper class to build {@code PerHostPercentileTracker} instances with a fluent interface. */ - public static class Builder extends PercentileTracker.Builder { - - Builder(long highestTrackableLatencyMillis) { - super(highestTrackableLatencyMillis); - } - - @Override - protected Builder self() { - return this; - } - - @Override - public PerHostPercentileTracker build() { - return new PerHostPercentileTracker( - highestTrackableLatencyMillis, - numberOfSignificantValueDigits, - minRecordedValues, - intervalMs); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/PercentileTracker.java b/driver-core/src/main/java/com/datastax/driver/core/PercentileTracker.java deleted file mode 100644 index 5821866d11f..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/PercentileTracker.java +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.google.common.base.Preconditions.checkArgument; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.concurrent.TimeUnit.MINUTES; -import static java.util.concurrent.TimeUnit.NANOSECONDS; - -import com.datastax.driver.core.exceptions.BootstrappingException; -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.exceptions.OverloadedException; -import com.datastax.driver.core.exceptions.QueryValidationException; -import com.datastax.driver.core.exceptions.UnavailableException; -import com.datastax.driver.core.exceptions.UnpreparedException; -import com.google.common.collect.ImmutableSet; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import org.HdrHistogram.Histogram; -import org.HdrHistogram.Recorder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A {@link LatencyTracker} that records query latencies over a sliding time interval, and exposes - * an API to retrieve the latency at a given percentile. - * - *

Percentiles may be computed separately for different categories of requests; this is - * implementation-dependent and determined by {@link #computeKey(Host, Statement, Exception)}. - * - *

This class is used by percentile-aware components such as {@link - * QueryLogger.Builder#withDynamicThreshold(PercentileTracker, double)} QueryLogger} and {@link - * com.datastax.driver.core.policies.PercentileSpeculativeExecutionPolicy}. - * - *

It uses HdrHistogram to record - * latencies: for each category, there is a "live" histogram where current latencies are recorded, - * and a "cached", read-only histogram that is used when clients call {@link - * #getLatencyAtPercentile(Host, Statement, Exception, double)}. Each time the cached histogram - * becomes older than the interval, the two histograms are switched. Statistics will not be - * available during the first interval at cluster startup, since we don't have a cached histogram - * yet. - */ -public abstract class PercentileTracker implements LatencyTracker { - private static final Logger logger = LoggerFactory.getLogger(PercentileTracker.class); - - private final long highestTrackableLatencyMillis; - private final int numberOfSignificantValueDigits; - private final int minRecordedValues; - private final long intervalMs; - - // The "live" recorders: this is where we store the latencies received from the cluster - private final ConcurrentMap recorders; - // The cached histograms, corresponding to the previous interval. This is where we get the - // percentiles from when the - // user requests them. Each histogram is valid for a given duration, when it gets stale we request - // a new one from - // the corresponding recorder. - private final ConcurrentMap cachedHistograms; - - /** - * Builds a new instance. - * - * @see Builder - */ - protected PercentileTracker( - long highestTrackableLatencyMillis, - int numberOfSignificantValueDigits, - int minRecordedValues, - long intervalMs) { - this.highestTrackableLatencyMillis = highestTrackableLatencyMillis; - this.numberOfSignificantValueDigits = numberOfSignificantValueDigits; - this.minRecordedValues = minRecordedValues; - this.intervalMs = intervalMs; - this.recorders = new ConcurrentHashMap(); - this.cachedHistograms = new ConcurrentHashMap(); - } - - /** - * Computes a key used to categorize measurements. Measurements with the same key will be recorded - * in the same histogram. - * - *

It's recommended to keep the number of distinct keys low, in order to limit the memory - * footprint of the histograms. - * - * @param host the host that was queried. - * @param statement the statement that was executed. - * @param exception if the query failed, the corresponding exception. - * @return the key. - */ - protected abstract Object computeKey(Host host, Statement statement, Exception exception); - - @Override - public void update(Host host, Statement statement, Exception exception, long newLatencyNanos) { - if (!include(host, statement, exception)) return; - - long latencyMs = NANOSECONDS.toMillis(newLatencyNanos); - try { - Recorder recorder = getRecorder(host, statement, exception); - if (recorder != null) recorder.recordValue(latencyMs); - } catch (ArrayIndexOutOfBoundsException e) { - logger.warn( - "Got request with latency of {} ms, which exceeds the configured maximum trackable value {}", - latencyMs, - highestTrackableLatencyMillis); - } - } - - /** - * Returns the request latency at a given percentile. - * - * @param host the host (if this is relevant in the way percentiles are categorized). - * @param statement the statement (if this is relevant in the way percentiles are categorized). - * @param exception the exception (if this is relevant in the way percentiles are categorized). - * @param percentile the percentile (for example, {@code 99.0} for the 99th percentile). - * @return the latency (in milliseconds) at the given percentile, or a negative value if it's not - * available yet. - * @see #computeKey(Host, Statement, Exception) - */ - public long getLatencyAtPercentile( - Host host, Statement statement, Exception exception, double percentile) { - checkArgument( - percentile >= 0.0 && percentile < 100, - "percentile must be between 0.0 and 100 (was %s)", - percentile); - Histogram histogram = getLastIntervalHistogram(host, statement, exception); - if (histogram == null || histogram.getTotalCount() < minRecordedValues) return -1; - - return histogram.getValueAtPercentile(percentile); - } - - private Recorder getRecorder(Host host, Statement statement, Exception exception) { - Object key = computeKey(host, statement, exception); - if (key == null) return null; - - Recorder recorder = recorders.get(key); - if (recorder == null) { - recorder = new Recorder(highestTrackableLatencyMillis, numberOfSignificantValueDigits); - Recorder old = recorders.putIfAbsent(key, recorder); - if (old != null) { - // We got beaten at creating the recorder, use the actual instance and discard ours - recorder = old; - } else { - // Also set an empty cache entry to remember the time we started recording: - cachedHistograms.putIfAbsent(key, CachedHistogram.empty()); - } - } - return recorder; - } - - /** @return null if no histogram is available yet (no entries recorded, or not for long enough) */ - private Histogram getLastIntervalHistogram(Host host, Statement statement, Exception exception) { - Object key = computeKey(host, statement, exception); - if (key == null) return null; - - try { - while (true) { - CachedHistogram entry = cachedHistograms.get(key); - if (entry == null) return null; - - long age = System.currentTimeMillis() - entry.timestamp; - if (age < intervalMs) { // current histogram is recent enough - return entry.histogram.get(); - } else { // need to refresh - Recorder recorder = recorders.get(key); - // intervalMs should be much larger than the time it takes to replace a histogram, so this - // future should never block - Histogram staleHistogram = entry.histogram.get(0, MILLISECONDS); - SettableFuture future = SettableFuture.create(); - CachedHistogram newEntry = new CachedHistogram(future); - if (cachedHistograms.replace(key, entry, newEntry)) { - // Only get the new histogram if we successfully replaced the cache entry. - // This ensures that only one thread will do it. - Histogram newHistogram = recorder.getIntervalHistogram(staleHistogram); - future.set(newHistogram); - return newHistogram; - } - // If we couldn't replace the entry it means we raced, so loop to try again - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return null; - } catch (ExecutionException e) { - throw new DriverInternalError("Unexpected error", e.getCause()); - } catch (TimeoutException e) { - throw new DriverInternalError("Unexpected timeout while getting histogram", e); - } - } - - /** - * A histogram and the timestamp at which it was retrieved. The data is only relevant for - * (timestamp + intervalMs); after that, the histogram is stale and we want to retrieve a new one. - */ - static class CachedHistogram { - final ListenableFuture histogram; - final long timestamp; - - CachedHistogram(ListenableFuture histogram) { - this.histogram = histogram; - this.timestamp = System.currentTimeMillis(); - } - - static CachedHistogram empty() { - return new CachedHistogram(Futures.immediateFuture(null)); - } - } - - @Override - public void onRegister(Cluster cluster) { - // nothing by default - } - - @Override - public void onUnregister(Cluster cluster) { - // nothing by default - } - - /** - * Determines whether a particular measurement should be included. - * - *

This is used to ignore measurements that could skew the statistics; for example, we - * typically want to ignore invalid query errors because they have a very low latency and would - * make a given cluster/host appear faster than it really is. - * - * @param host the host that was queried. - * @param statement the statement that was executed. - * @param exception if the query failed, the corresponding exception. - * @return whether the measurement should be included. - */ - protected boolean include(Host host, Statement statement, Exception exception) { - // query was successful: always consider - if (exception == null) return true; - // filter out "fast" errors - // TODO this was copy/pasted from LatencyAwarePolicy, maybe it could be refactored as a shared - // method - return !EXCLUDED_EXCEPTIONS.contains(exception.getClass()); - } - - /** - * A set of DriverException subclasses that we should prevent from updating the host's score. The - * intent behind it is to filter out "fast" errors: when a host replies with such errors, it - * usually does so very quickly, because it did not involve any actual coordination work. Such - * errors are not good indicators of the host's responsiveness, and tend to make the host's score - * look better than it actually is. - */ - @SuppressWarnings("unchecked") - private static final Set> EXCLUDED_EXCEPTIONS = - ImmutableSet.of( - UnavailableException.class, // this is done via the snitch and is usually very fast - OverloadedException.class, - BootstrappingException.class, - UnpreparedException.class, - QueryValidationException - .class, // query validation also happens at early stages in the coordinator - CancelledSpeculativeExecutionException.class); - - /** - * Base class for {@code PercentileTracker} implementation builders. - * - * @param the type of the concrete builder implementation. - * @param the type of the object to build. - */ - public abstract static class Builder { - protected final long highestTrackableLatencyMillis; - protected int numberOfSignificantValueDigits = 3; - protected int minRecordedValues = 1000; - protected long intervalMs = MINUTES.toMillis(5); - - Builder(long highestTrackableLatencyMillis) { - this.highestTrackableLatencyMillis = highestTrackableLatencyMillis; - } - - protected abstract B self(); - - /** - * Sets the number of significant decimal digits to which histograms will maintain value - * resolution and separation. This must be an integer between 0 and 5. - * - *

If not set explicitly, this value defaults to 3. - * - *

See the - * HdrHistogram Javadocs for a more detailed explanation on how this parameter affects the - * resolution of recorded samples. - * - * @param numberOfSignificantValueDigits the new value. - * @return this builder. - */ - public B withNumberOfSignificantValueDigits(int numberOfSignificantValueDigits) { - this.numberOfSignificantValueDigits = numberOfSignificantValueDigits; - return self(); - } - - /** - * Sets the minimum number of values that must be recorded for a host before we consider the - * sample size significant. - * - *

If this count is not reached during a given interval, {@link #getLatencyAtPercentile(Host, - * Statement, Exception, double)} will return a negative value, indicating that statistics are - * not available. In particular, this is true during the first interval. - * - *

If not set explicitly, this value default to 1000. - * - * @param minRecordedValues the new value. - * @return this builder. - */ - public B withMinRecordedValues(int minRecordedValues) { - this.minRecordedValues = minRecordedValues; - return self(); - } - - /** - * Sets the time interval over which samples are recorded. - * - *

For each host, there is a "live" histogram where current latencies are recorded, and a - * "cached", read-only histogram that is used when clients call {@link - * #getLatencyAtPercentile(Host, Statement, Exception, double)}. Each time the cached histogram - * becomes older than the interval, the two histograms are switched. Note that statistics will - * not be available during the first interval at cluster startup, since we don't have a cached - * histogram yet. - * - *

If not set explicitly, this value defaults to 5 minutes. - * - * @param interval the new interval. - * @param unit the unit that the interval is expressed in. - * @return this builder. - */ - public B withInterval(long interval, TimeUnit unit) { - this.intervalMs = MILLISECONDS.convert(interval, unit); - return self(); - } - - /** - * Builds the {@code PercentileTracker} instance configured with this builder. - * - * @return the instance. - */ - public abstract T build(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/PlainTextAuthProvider.java b/driver-core/src/main/java/com/datastax/driver/core/PlainTextAuthProvider.java deleted file mode 100644 index 49030c51084..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/PlainTextAuthProvider.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.AuthenticationException; -import com.google.common.base.Charsets; -import com.google.common.collect.ImmutableMap; -import java.net.InetSocketAddress; -import java.util.Map; - -/** - * A simple {@code AuthProvider} implementation. - * - *

This provider allows to programmatically define authentication information that will then - * apply to all hosts. The PlainTextAuthenticator instances it returns support SASL authentication - * using the PLAIN mechanism for version 2 (or above) of the CQL native protocol. - */ -public class PlainTextAuthProvider implements ExtendedAuthProvider { - - private volatile String username; - private volatile String password; - - /** - * Creates a new simple authentication information provider with the supplied credentials. - * - * @param username to use for authentication requests - * @param password to use for authentication requests - */ - public PlainTextAuthProvider(String username, String password) { - this.username = username; - this.password = password; - } - - /** - * Changes the user name. - * - *

The new credentials will be used for all connections initiated after this method was called. - * - * @param username the new name. - */ - public void setUsername(String username) { - this.username = username; - } - - /** - * Changes the password. - * - *

The new credentials will be used for all connections initiated after this method was called. - * - * @param password the new password. - */ - public void setPassword(String password) { - this.password = password; - } - - /** - * Uses the supplied credentials and the SASL PLAIN mechanism to login to the server. - * - * @param host the Cassandra host with which we want to authenticate - * @param authenticator the configured authenticator on the host - * @return an Authenticator instance which can be used to perform authentication negotiations on - * behalf of the client - */ - @Override - public Authenticator newAuthenticator(EndPoint host, String authenticator) { - return new PlainTextAuthenticator(username, password); - } - - @Override - public Authenticator newAuthenticator(InetSocketAddress host, String authenticator) - throws AuthenticationException { - throw new AssertionError( - "The driver should never call this method on an object that implements " - + this.getClass().getSimpleName()); - } - - /** - * Simple implementation of {@link Authenticator} which can perform authentication against - * Cassandra servers configured with PasswordAuthenticator. - */ - static class PlainTextAuthenticator extends ProtocolV1Authenticator implements Authenticator { - - private final byte[] username; - private final byte[] password; - - public PlainTextAuthenticator(String username, String password) { - this.username = username.getBytes(Charsets.UTF_8); - this.password = password.getBytes(Charsets.UTF_8); - } - - @Override - public byte[] initialResponse() { - byte[] initialToken = new byte[username.length + password.length + 2]; - initialToken[0] = 0; - System.arraycopy(username, 0, initialToken, 1, username.length); - initialToken[username.length + 1] = 0; - System.arraycopy(password, 0, initialToken, username.length + 2, password.length); - return initialToken; - } - - @Override - public byte[] evaluateChallenge(byte[] challenge) { - return null; - } - - @Override - public void onAuthenticationSuccess(byte[] token) { - // no-op, the server should send nothing anyway - } - - @Override - Map getCredentials() { - return ImmutableMap.of( - "username", - new String(username, Charsets.UTF_8), - "password", - new String(password, Charsets.UTF_8)); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/PoolingOptions.java b/driver-core/src/main/java/com/datastax/driver/core/PoolingOptions.java deleted file mode 100644 index 3741ef97ec0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/PoolingOptions.java +++ /dev/null @@ -1,664 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.HostDistance.LOCAL; -import static com.datastax.driver.core.HostDistance.REMOTE; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import java.util.Map; -import java.util.concurrent.Executor; - -/** - * Options related to connection pooling. - * - *

The driver uses connections in an asynchronous manner, meaning that multiple requests can be - * submitted on the same connection at the same time. Therefore only a relatively small number of - * connections is needed. For each host, the driver uses a connection pool that may have a variable - * size (it will automatically adjust to the current load). - * - *

With {@code ProtocolVersion#V2} or below, there are at most 128 simultaneous requests per - * connection, so the pool defaults to a variable size. You will typically raise the maximum - * capacity by adding more connections with {@link #setMaxConnectionsPerHost(HostDistance, int)}. - * - *

With {@code ProtocolVersion#V3} or above, there are up to 32768 requests per connection, and - * the pool defaults to a fixed size of 1. You will typically raise the maximum capacity by allowing - * more simultaneous requests per connection ({@link #setMaxRequestsPerConnection(HostDistance, - * int)}). - * - *

All parameters can be separately set for {@code LOCAL} and {@code REMOTE} hosts ({@link - * HostDistance}). For {@code IGNORED} hosts, no connections are created so these settings cannot be - * changed. - */ -public class PoolingOptions { - - /** - * The value returned for connection options when they have not been set by the client, and the - * protocol version is not known yet. - * - *

Once a {@code PoolingOptions} object is associated to a {@link Cluster} and that cluster - * initializes, the protocol version will be detected, and connection options will take their - * default values for that protocol version. - * - *

The methods that may return this value are: {@link - * #getCoreConnectionsPerHost(HostDistance)}, {@link #getMaxConnectionsPerHost(HostDistance)}, - * {@link #getNewConnectionThreshold(HostDistance)}, {@link - * #getMaxRequestsPerConnection(HostDistance)}. - */ - public static final int UNSET = Integer.MIN_VALUE; - - public static final String CORE_POOL_LOCAL_KEY = "corePoolLocal"; - public static final String MAX_POOL_LOCAL_KEY = "maxPoolLocal"; - public static final String CORE_POOL_REMOTE_KEY = "corePoolRemote"; - public static final String MAX_POOL_REMOTE_KEY = "maxPoolRemote"; - public static final String NEW_CONNECTION_THRESHOLD_LOCAL_KEY = "newConnectionThresholdLocal"; - public static final String NEW_CONNECTION_THRESHOLD_REMOTE_KEY = "newConnectionThresholdRemote"; - public static final String MAX_REQUESTS_PER_CONNECTION_LOCAL_KEY = - "maxRequestsPerConnectionLocal"; - public static final String MAX_REQUESTS_PER_CONNECTION_REMOTE_KEY = - "maxRequestsPerConnectionRemote"; - - /** - * The default values for connection options, that depend on the native protocol version. - * - *

The map stores protocol versions in ascending order, and only the versions that introduced a - * change are present. To find the defaults for a particular version, look for the highest key - * that is less than or equal to that version, in other words: - * - *

{@code
-   * ProtocolVersion referenceVersion = null;
-   * for (ProtocolVersion key : DEFAULTS.keySet()) {
-   *     if (key.compareTo(actualVersion) > 0)
-   *         break;
-   *     else
-   *         referenceVersion = key;
-   * }
-   * Map defaults = DEFAULTS.get(referenceVersion);
-   * }
- * - * Once you've extracted the underlying map, use the keys {@code CORE_POOL_LOCAL_KEY}, {@code - * MAX_POOL_LOCAL_KEY}, {@code CORE_POOL_REMOTE_KEY}, {@code MAX_POOL_REMOTE_KEY}, {@code - * NEW_CONNECTION_THRESHOLD_LOCAL_KEY}, {@code NEW_CONNECTION_THRESHOLD_REMOTE_KEY}, {@code - * MAX_REQUESTS_PER_CONNECTION_LOCAL_KEY} and {@code MAX_REQUESTS_PER_CONNECTION_REMOTE_KEY}. - * - * @see #UNSET - */ - public static final Map> DEFAULTS = - ImmutableMap.>of( - ProtocolVersion.V1, - ImmutableMap.builder() - .put(CORE_POOL_LOCAL_KEY, 2) - .put(MAX_POOL_LOCAL_KEY, 8) - .put(CORE_POOL_REMOTE_KEY, 1) - .put(MAX_POOL_REMOTE_KEY, 2) - .put(NEW_CONNECTION_THRESHOLD_LOCAL_KEY, 100) - .put(NEW_CONNECTION_THRESHOLD_REMOTE_KEY, 100) - .put(MAX_REQUESTS_PER_CONNECTION_LOCAL_KEY, 128) - .put(MAX_REQUESTS_PER_CONNECTION_REMOTE_KEY, 128) - .build(), - ProtocolVersion.V3, - ImmutableMap.builder() - .put(CORE_POOL_LOCAL_KEY, 1) - .put(MAX_POOL_LOCAL_KEY, 1) - .put(CORE_POOL_REMOTE_KEY, 1) - .put(MAX_POOL_REMOTE_KEY, 1) - .put(NEW_CONNECTION_THRESHOLD_LOCAL_KEY, 800) - .put(NEW_CONNECTION_THRESHOLD_REMOTE_KEY, 200) - .put(MAX_REQUESTS_PER_CONNECTION_LOCAL_KEY, 1024) - .put(MAX_REQUESTS_PER_CONNECTION_REMOTE_KEY, 256) - .build()); - - /** The default value for {@link #getIdleTimeoutSeconds()} ({@value}). */ - public static final int DEFAULT_IDLE_TIMEOUT_SECONDS = 120; - - /** The default value for {@link #getPoolTimeoutMillis()} ({@value}). */ - public static final int DEFAULT_POOL_TIMEOUT_MILLIS = 5000; - - /** The default value for {@link #getMaxQueueSize()} ({@value}). */ - public static final int DEFAULT_MAX_QUEUE_SIZE = 256; - - /** The default value for {@link #getHeartbeatIntervalSeconds()} ({@value}). */ - public static final int DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 30; - - private static final Executor DEFAULT_INITIALIZATION_EXECUTOR = - GuavaCompatibility.INSTANCE.sameThreadExecutor(); - - private volatile Cluster.Manager manager; - private volatile ProtocolVersion protocolVersion; - - // The defaults for these fields depend on the protocol version, which is only known after control - // connection initialization. - // Yet if the user set them before initialization, we want to keep their values. So we use -1 to - // mean "uninitialized". - private final int[] coreConnections = new int[] {UNSET, UNSET, 0}; - private final int[] maxConnections = new int[] {UNSET, UNSET, 0}; - private final int[] newConnectionThreshold = new int[] {UNSET, UNSET, 0}; - private volatile int maxRequestsPerConnectionLocal = UNSET; - private volatile int maxRequestsPerConnectionRemote = UNSET; - - private volatile int idleTimeoutSeconds = DEFAULT_IDLE_TIMEOUT_SECONDS; - private volatile int poolTimeoutMillis = DEFAULT_POOL_TIMEOUT_MILLIS; - private volatile int maxQueueSize = DEFAULT_MAX_QUEUE_SIZE; - private volatile int heartbeatIntervalSeconds = DEFAULT_HEARTBEAT_INTERVAL_SECONDS; - - private volatile Executor initializationExecutor = DEFAULT_INITIALIZATION_EXECUTOR; - - public PoolingOptions() {} - - void register(Cluster.Manager manager) { - this.manager = manager; - } - - /** - * Returns the core number of connections per host. - * - * @param distance the {@code HostDistance} for which to return this threshold. - * @return the core number of connections per host at distance {@code distance}. - */ - public int getCoreConnectionsPerHost(HostDistance distance) { - return coreConnections[distance.ordinal()]; - } - - /** - * Sets the core number of connections per host. - * - *

For the provided {@code distance}, this corresponds to the number of connections initially - * created and kept open to each host of that distance. - * - *

The default value is: - * - *

    - *
  • with {@code ProtocolVersion#V2} or below: 2 for {@code LOCAL} hosts and 1 for {@code - * REMOTE} hosts. - *
  • with {@code ProtocolVersion#V3} or above: 1 for all hosts. - *
- * - * @param distance the {@code HostDistance} for which to set this threshold. - * @param newCoreConnections the value to set - * @return this {@code PoolingOptions}. - * @throws IllegalArgumentException if {@code distance == HostDistance.IGNORED}, or if {@code - * newCoreConnections} is greater than the maximum value for this distance. - * @see #setConnectionsPerHost(HostDistance, int, int) - */ - public synchronized PoolingOptions setCoreConnectionsPerHost( - HostDistance distance, int newCoreConnections) { - if (distance == HostDistance.IGNORED) - throw new IllegalArgumentException( - "Cannot set core connections per host for " + distance + " hosts"); - Preconditions.checkArgument( - newCoreConnections >= 0, "core number of connections must be positive"); - - if (maxConnections[distance.ordinal()] != UNSET) - checkConnectionsPerHostOrder( - newCoreConnections, maxConnections[distance.ordinal()], distance); - - int oldCore = coreConnections[distance.ordinal()]; - coreConnections[distance.ordinal()] = newCoreConnections; - if (oldCore < newCoreConnections && manager != null) manager.ensurePoolsSizing(); - return this; - } - - /** - * Returns the maximum number of connections per host. - * - * @param distance the {@code HostDistance} for which to return this threshold. - * @return the maximum number of connections per host at distance {@code distance}. - */ - public int getMaxConnectionsPerHost(HostDistance distance) { - return maxConnections[distance.ordinal()]; - } - - /** - * Sets the maximum number of connections per host. - * - *

For the provided {@code distance}, this corresponds to the maximum number of connections - * that can be created per host at that distance. - * - *

The default value is: - * - *

    - *
  • with {@code ProtocolVersion#V2} or below: 8 for {@code LOCAL} hosts and 2 for {@code - * REMOTE} hosts. - *
  • with {@code ProtocolVersion#V3} or above: 1 for all hosts. - *
- * - * @param distance the {@code HostDistance} for which to set this threshold. - * @param newMaxConnections the value to set - * @return this {@code PoolingOptions}. - * @throws IllegalArgumentException if {@code distance == HostDistance.IGNORED}, or if {@code - * newMaxConnections} is less than the core value for this distance. - * @see #setConnectionsPerHost(HostDistance, int, int) - */ - public synchronized PoolingOptions setMaxConnectionsPerHost( - HostDistance distance, int newMaxConnections) { - if (distance == HostDistance.IGNORED) - throw new IllegalArgumentException( - "Cannot set max connections per host for " + distance + " hosts"); - Preconditions.checkArgument( - newMaxConnections >= 0, "max number of connections must be positive"); - - if (coreConnections[distance.ordinal()] != UNSET) - checkConnectionsPerHostOrder( - coreConnections[distance.ordinal()], newMaxConnections, distance); - - maxConnections[distance.ordinal()] = newMaxConnections; - return this; - } - - /** - * Sets the core and maximum number of connections per host in one call. - * - *

This is a convenience method that is equivalent to calling {@link - * #setCoreConnectionsPerHost(HostDistance, int)} and {@link - * #setMaxConnectionsPerHost(HostDistance, int)}. - * - * @param distance the {@code HostDistance} for which to set these threshold. - * @param core the core number of connections. - * @param max the max number of connections. - * @return this {@code PoolingOptions}. - * @throws IllegalArgumentException if {@code distance == HostDistance.IGNORED}, or if {@code - * core} > {@code max}. - */ - public synchronized PoolingOptions setConnectionsPerHost( - HostDistance distance, int core, int max) { - if (distance == HostDistance.IGNORED) - throw new IllegalArgumentException( - "Cannot set connections per host for " + distance + " hosts"); - Preconditions.checkArgument(core >= 0, "core number of connections must be positive"); - Preconditions.checkArgument(max >= 0, "max number of connections must be positive"); - - checkConnectionsPerHostOrder(core, max, distance); - coreConnections[distance.ordinal()] = core; - maxConnections[distance.ordinal()] = max; - return this; - } - - /** - * Returns the threshold that triggers the creation of a new connection to a host. - * - * @param distance the {@code HostDistance} for which to return this threshold. - * @return the configured threshold, or the default one if none have been set. - * @see #setNewConnectionThreshold(HostDistance, int) - */ - public int getNewConnectionThreshold(HostDistance distance) { - return newConnectionThreshold[distance.ordinal()]; - } - - /** - * Sets the threshold that triggers the creation of a new connection to a host. - * - *

A new connection gets created if: - * - *

    - *
  • N connections are open - *
  • N < {@link #getMaxConnectionsPerHost(HostDistance)} - *
  • the number of active requests is more than (N - 1) * {@link - * #getMaxRequestsPerConnection(HostDistance)} + {@link - * #getNewConnectionThreshold(HostDistance)} - *
- * - * In other words, if all but the last connection are full, and the last connection is above this - * threshold. - * - *

The default value is: - * - *

    - *
  • with {@code ProtocolVersion#V2} or below: 100 for all hosts. - *
  • with {@code ProtocolVersion#V3} or above: 800 for {@code LOCAL} hosts and 200 for {@code - * REMOTE} hosts. - *
- * - * @param distance the {@code HostDistance} for which to configure this threshold. - * @param newValue the value to set (between 0 and 128). - * @return this {@code PoolingOptions}. - * @throws IllegalArgumentException if {@code distance == HostDistance.IGNORED}, or if {@code - * maxSimultaneousRequests} is not in range, or if {@code newValue} is less than the minimum - * value for this distance. - */ - public synchronized PoolingOptions setNewConnectionThreshold( - HostDistance distance, int newValue) { - if (distance == HostDistance.IGNORED) - throw new IllegalArgumentException( - "Cannot set new connection threshold for " + distance + " hosts"); - - checkRequestsPerConnectionRange(newValue, "New connection threshold", distance); - newConnectionThreshold[distance.ordinal()] = newValue; - return this; - } - - /** - * Returns the maximum number of requests per connection. - * - * @param distance the {@code HostDistance} for which to return this threshold. - * @return the maximum number of requests per connection at distance {@code distance}. - * @see #setMaxRequestsPerConnection(HostDistance, int) - */ - public int getMaxRequestsPerConnection(HostDistance distance) { - switch (distance) { - case LOCAL: - return maxRequestsPerConnectionLocal; - case REMOTE: - return maxRequestsPerConnectionRemote; - default: - return 0; - } - } - - /** - * Sets the maximum number of requests per connection. - * - *

The default value is: - * - *

    - *
  • with {@code ProtocolVersion#V2} or below: 128 for all hosts (there should not be any - * reason to change this). - *
  • with {@code ProtocolVersion#V3} or above: 1024 for {@code LOCAL} hosts and 256 for {@code - * REMOTE} hosts. These values were chosen so that the default V2 and V3 configuration - * generate the same load on a Cassandra cluster. Protocol V3 can go much higher (up to - * 32768), so if your number of clients is low, don't hesitate to experiment with higher - * values. If you have more than one connection per host, consider also adjusting {@link - * #setNewConnectionThreshold(HostDistance, int)}. - *
- * - * @param distance the {@code HostDistance} for which to set this threshold. - * @param newMaxRequests the value to set. - * @return this {@code PoolingOptions}. - * @throws IllegalArgumentException if {@code distance == HostDistance.IGNORED}, or if {@code - * newMaxConnections} is not within the allowed range. - */ - public PoolingOptions setMaxRequestsPerConnection(HostDistance distance, int newMaxRequests) { - checkRequestsPerConnectionRange(newMaxRequests, "Max requests per connection", distance); - - switch (distance) { - case LOCAL: - maxRequestsPerConnectionLocal = newMaxRequests; - break; - case REMOTE: - maxRequestsPerConnectionRemote = newMaxRequests; - break; - default: - throw new IllegalArgumentException( - "Cannot set max requests per host for " + distance + " hosts"); - } - return this; - } - - /** - * Returns the timeout before an idle connection is removed. - * - * @return the timeout. - */ - public int getIdleTimeoutSeconds() { - return idleTimeoutSeconds; - } - - /** - * Sets the timeout before an idle connection is removed. - * - *

The order of magnitude should be a few minutes (the default is 120 seconds). The timeout - * that triggers the removal has a granularity of 10 seconds. - * - * @param idleTimeoutSeconds the new timeout in seconds. - * @return this {@code PoolingOptions}. - * @throws IllegalArgumentException if the timeout is negative. - */ - public PoolingOptions setIdleTimeoutSeconds(int idleTimeoutSeconds) { - if (idleTimeoutSeconds < 0) throw new IllegalArgumentException("Idle timeout must be positive"); - this.idleTimeoutSeconds = idleTimeoutSeconds; - return this; - } - - /** - * Returns the timeout when trying to acquire a connection from a host's pool. - * - * @return the timeout. - */ - public int getPoolTimeoutMillis() { - return poolTimeoutMillis; - } - - /** - * Sets the timeout when trying to acquire a connection from a host's pool. - * - *

This option works in concert with {@link #setMaxQueueSize(int)} to determine what happens if - * the driver tries to borrow a connection from the pool but none is available: - * - *

    - *
  • if either option is set to zero, the attempt is rejected immediately; - *
  • else if more than {@code maxQueueSize} requests are already waiting for a connection, the - * attempt is also rejected; - *
  • otherwise, the attempt is enqueued; if a connection becomes available before {@code - * poolTimeoutMillis} has elapsed, then the attempt succeeds, otherwise it is rejected. - *
- * - * If the attempt is rejected, the driver will move to the next host in the {@link - * com.datastax.driver.core.policies.LoadBalancingPolicy#newQueryPlan(String, Statement)} query - * plan}. - * - *

The default is 5 seconds. If this option is set to zero, the driver won't wait at all. - * - * @param poolTimeoutMillis the new value in milliseconds. - * @return this {@code PoolingOptions} - * @throws IllegalArgumentException if the timeout is negative. - */ - public PoolingOptions setPoolTimeoutMillis(int poolTimeoutMillis) { - if (poolTimeoutMillis < 0) throw new IllegalArgumentException("Pool timeout must be positive"); - this.poolTimeoutMillis = poolTimeoutMillis; - return this; - } - - /** - * Returns the maximum number of requests that get enqueued if no connection is available. - * - * @return the maximum queue size. - */ - public int getMaxQueueSize() { - return maxQueueSize; - } - - /** - * Sets the maximum number of requests that get enqueued if no connection is available. - * - *

This option works in concert with {@link #setPoolTimeoutMillis(int)} to determine what - * happens if the driver tries to borrow a connection from the pool but none is available: - * - *

    - *
  • if either options is set to zero, the attempt is rejected immediately; - *
  • else if more than {@code maxQueueSize} requests are already waiting for a connection, the - * attempt is also rejected; - *
  • otherwise, the attempt is enqueued; if a connection becomes available before {@code - * poolTimeoutMillis} has elapsed, then the attempt succeeds, otherwise it is rejected. - *
- * - * If the attempt is rejected, the driver will move to the next host in the {@link - * com.datastax.driver.core.policies.LoadBalancingPolicy#newQueryPlan(String, Statement)} query - * plan}. - * - *

The default value is {@value DEFAULT_MAX_QUEUE_SIZE}. If this option is set to zero, the - * driver will never enqueue requests. - * - * @param maxQueueSize the new value. - * @return this {@code PoolingOptions} - * @throws IllegalArgumentException if the value is negative. - */ - public PoolingOptions setMaxQueueSize(int maxQueueSize) { - if (maxQueueSize < 0) throw new IllegalArgumentException("Max queue size must be positive"); - this.maxQueueSize = maxQueueSize; - return this; - } - - /** - * Returns the heart beat interval, after which a message is sent on an idle connection to make - * sure it's still alive. - * - * @return the interval. - */ - public int getHeartbeatIntervalSeconds() { - return heartbeatIntervalSeconds; - } - - /** - * Sets the heart beat interval, after which a message is sent on an idle connection to make sure - * it's still alive. - * - *

This is an application-level keep-alive, provided for convenience since adjusting the TCP - * keep-alive might not be practical in all environments. - * - *

This option should be set higher than {@link SocketOptions#getReadTimeoutMillis()}. - * - *

The default value for this option is 30 seconds. - * - * @param heartbeatIntervalSeconds the new value in seconds. If set to 0, it will disable the - * feature. - * @return this {@code PoolingOptions} - * @throws IllegalArgumentException if the interval is negative. - */ - public PoolingOptions setHeartbeatIntervalSeconds(int heartbeatIntervalSeconds) { - if (heartbeatIntervalSeconds < 0) - throw new IllegalArgumentException("Heartbeat interval must be positive"); - - this.heartbeatIntervalSeconds = heartbeatIntervalSeconds; - return this; - } - - /** - * Returns the executor to use for connection initialization. - * - * @return the executor. - * @see #setInitializationExecutor(java.util.concurrent.Executor) - */ - public Executor getInitializationExecutor() { - return initializationExecutor; - } - - /** - * Sets the executor to use for connection initialization. - * - *

Connections are open in a completely asynchronous manner. Since initializing the transport - * requires separate CQL queries, the futures representing the completion of these queries are - * transformed and chained. This executor is where these transformations happen. - * - *

This is an advanced option, which should be rarely needed in practice. It defaults to - * Guava's {@code MoreExecutors.sameThreadExecutor()}, which results in running the - * transformations on the network I/O threads; this is fine if the transformations are fast and - * not I/O bound (which is the case by default). One reason why you might want to provide a custom - * executor is if you use authentication with a custom {@link - * com.datastax.driver.core.Authenticator} implementation that performs blocking calls. - * - * @param initializationExecutor the executor to use - * @return this {@code PoolingOptions} - * @throws java.lang.NullPointerException if the executor is null - */ - public PoolingOptions setInitializationExecutor(Executor initializationExecutor) { - Preconditions.checkNotNull(initializationExecutor); - this.initializationExecutor = initializationExecutor; - return this; - } - - synchronized void setProtocolVersion(ProtocolVersion actualVersion) { - this.protocolVersion = actualVersion; - - ProtocolVersion referenceVersion = null; - for (ProtocolVersion key : DEFAULTS.keySet()) { - if (key.compareTo(actualVersion) > 0) break; - else referenceVersion = key; - } - assert referenceVersion != null; // will not happen since V1 is a key - - Map defaults = DEFAULTS.get(referenceVersion); - - if (coreConnections[LOCAL.ordinal()] == UNSET) - coreConnections[LOCAL.ordinal()] = defaults.get(CORE_POOL_LOCAL_KEY); - if (maxConnections[LOCAL.ordinal()] == UNSET) - maxConnections[LOCAL.ordinal()] = defaults.get(MAX_POOL_LOCAL_KEY); - checkConnectionsPerHostOrder( - coreConnections[LOCAL.ordinal()], maxConnections[LOCAL.ordinal()], LOCAL); - - if (coreConnections[REMOTE.ordinal()] == UNSET) - coreConnections[REMOTE.ordinal()] = defaults.get(CORE_POOL_REMOTE_KEY); - if (maxConnections[REMOTE.ordinal()] == UNSET) - maxConnections[REMOTE.ordinal()] = defaults.get(MAX_POOL_REMOTE_KEY); - checkConnectionsPerHostOrder( - coreConnections[REMOTE.ordinal()], maxConnections[REMOTE.ordinal()], REMOTE); - - if (newConnectionThreshold[LOCAL.ordinal()] == UNSET) - newConnectionThreshold[LOCAL.ordinal()] = defaults.get(NEW_CONNECTION_THRESHOLD_LOCAL_KEY); - checkRequestsPerConnectionRange( - newConnectionThreshold[LOCAL.ordinal()], "New connection threshold", LOCAL); - - if (newConnectionThreshold[REMOTE.ordinal()] == UNSET) - newConnectionThreshold[REMOTE.ordinal()] = defaults.get(NEW_CONNECTION_THRESHOLD_REMOTE_KEY); - checkRequestsPerConnectionRange( - newConnectionThreshold[REMOTE.ordinal()], "New connection threshold", REMOTE); - - if (maxRequestsPerConnectionLocal == UNSET) - maxRequestsPerConnectionLocal = defaults.get(MAX_REQUESTS_PER_CONNECTION_LOCAL_KEY); - checkRequestsPerConnectionRange( - maxRequestsPerConnectionLocal, "Max requests per connection", LOCAL); - - if (maxRequestsPerConnectionRemote == UNSET) - maxRequestsPerConnectionRemote = defaults.get(MAX_REQUESTS_PER_CONNECTION_REMOTE_KEY); - checkRequestsPerConnectionRange( - maxRequestsPerConnectionRemote, "Max requests per connection", REMOTE); - } - - /** - * Requests the driver to re-evaluate the {@link HostDistance} (through the configured {@link - * com.datastax.driver.core.policies.LoadBalancingPolicy#distance}) for every known hosts and to - * drop/add connections to each hosts according to the computed distance. - * - *

Note that, due to backward compatibility issues, this method is not interruptible. If the - * caller thread gets interrupted, the method will complete and only then re-interrupt the thread - * (which you can check with {@code Thread.currentThread().isInterrupted()}). - */ - public void refreshConnectedHosts() { - manager.refreshConnectedHosts(); - } - - /** - * Requests the driver to re-evaluate the {@link HostDistance} for a given node. - * - * @param host the host to refresh. - * @see #refreshConnectedHosts() - */ - public void refreshConnectedHost(Host host) { - manager.refreshConnectedHost(host); - } - - private void checkRequestsPerConnectionRange( - int value, String description, HostDistance distance) { - // If we don't know the protocol version yet, use the highest possible upper bound, this will - // get checked again when possible - int max = - (protocolVersion == null || protocolVersion.compareTo(ProtocolVersion.V3) >= 0) - ? StreamIdGenerator.MAX_STREAM_PER_CONNECTION_V3 - : StreamIdGenerator.MAX_STREAM_PER_CONNECTION_V2; - - if (value < 0 || value > max) - throw new IllegalArgumentException( - String.format( - "%s for %s hosts must be in the range (0, %d)", description, distance, max)); - } - - private static void checkConnectionsPerHostOrder(int core, int max, HostDistance distance) { - if (core > max) - throw new IllegalArgumentException( - String.format( - "Core connections for %s hosts must be less than max (%d > %d)", - distance, core, max)); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/PreparedId.java b/driver-core/src/main/java/com/datastax/driver/core/PreparedId.java deleted file mode 100644 index 7e2e4a66b92..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/PreparedId.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** Identifies a PreparedStatement. */ -public class PreparedId { - - // This class is mostly here to group PreparedStatement data that are needed for - // execution but that we don't want to expose publicly (see JAVA-195) - - final int[] routingKeyIndexes; - - final ProtocolVersion protocolVersion; - - final PreparedMetadata boundValuesMetadata; - - // can change over time, see JAVA-1196, JAVA-420 - volatile PreparedMetadata resultSetMetadata; - - PreparedId( - PreparedMetadata boundValuesMetadata, - PreparedMetadata resultSetMetadata, - int[] routingKeyIndexes, - ProtocolVersion protocolVersion) { - assert boundValuesMetadata != null; - assert resultSetMetadata != null; - this.boundValuesMetadata = boundValuesMetadata; - this.resultSetMetadata = resultSetMetadata; - this.routingKeyIndexes = routingKeyIndexes; - this.protocolVersion = protocolVersion; - } - - static class PreparedMetadata { - - final MD5Digest id; - final ColumnDefinitions variables; - - PreparedMetadata(MD5Digest id, ColumnDefinitions variables) { - this.id = id; - this.variables = variables; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/PreparedStatement.java b/driver-core/src/main/java/com/datastax/driver/core/PreparedStatement.java deleted file mode 100644 index 3f2d1cf6cf2..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/PreparedStatement.java +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.InvalidTypeException; -import com.datastax.driver.core.policies.RetryPolicy; -import java.nio.ByteBuffer; -import java.util.Map; - -/** - * Represents a prepared statement, a query with bound variables that has been prepared (pre-parsed) - * by the database. - * - *

A prepared statement can be executed once concrete values have been provided for the bound - * variables. A prepared statement and the values for its bound variables constitute a - * BoundStatement and can be executed (by {@link Session#execute}). - * - *

A {@code PreparedStatement} object allows you to define specific defaults for the different - * properties of a {@link Statement} (Consistency level, tracing, ...), in which case those - * properties will be inherited as default by every BoundedStatement created from the - * {PreparedStatement}. The default for those {@code PreparedStatement} properties is the same that - * in {@link Statement} if the PreparedStatement is created by {@link Session#prepare(String)} but - * will inherit of the properties of the {@link RegularStatement} used for the preparation if {@link - * Session#prepare(RegularStatement)} is used. - */ -public interface PreparedStatement { - - /** - * Returns metadata on the bounded variables of this prepared statement. - * - * @return the variables bounded in this prepared statement. - */ - public ColumnDefinitions getVariables(); - - /** - * Creates a new BoundStatement object and bind its variables to the provided values. - * - *

While the number of {@code values} cannot be greater than the number of bound variables, the - * number of {@code values} may be fewer than the number of bound variables. In that case, the - * remaining variables will have to be bound to values by another mean because the resulting - * {@code BoundStatement} being executable. - * - *

This method is a convenience for {@code new BoundStatement(this).bind(...)}. - * - * @param values the values to bind to the variables of the newly created BoundStatement. - * @return the newly created {@code BoundStatement} with its variables bound to {@code values}. - * @throws IllegalArgumentException if more {@code values} are provided than there is of bound - * variables in this statement. - * @throws InvalidTypeException if any of the provided value is not of correct type to be bound to - * the corresponding bind variable. - * @throws NullPointerException if one of {@code values} is a collection (List, Set or Map) - * containing a null value. Nulls are not supported in collections by CQL. - * @see BoundStatement#bind - */ - public BoundStatement bind(Object... values); - - /** - * Creates a new BoundStatement object for this prepared statement. - * - *

This method do not bind any values to any of the prepared variables. Said values need to be - * bound on the resulting statement using BoundStatement's setters methods ({@link - * BoundStatement#setInt}, {@link BoundStatement#setLong}, ...). - * - * @return the newly created {@code BoundStatement}. - */ - public BoundStatement bind(); - - /** - * Sets the routing key for this prepared statement. - * - *

While you can provide a fixed routing key for all executions of this prepared statement with - * this method, it is not mandatory to provide one through this method. This method should only be - * used if the partition key of the prepared query is not part of the prepared variables (that is - * if the partition key is fixed). - * - *

Note that if the partition key is part of the prepared variables, the routing key will be - * automatically computed once those variables are bound. - * - *

If the partition key is neither fixed nor part of the prepared variables (e.g. a composite - * partition key where only some of the components are bound), the routing key can also be set on - * each bound statement. - * - * @param routingKey the raw (binary) value to use as routing key. - * @return this {@code PreparedStatement} object. - * @see Statement#getRoutingKey - * @see BoundStatement#getRoutingKey - */ - public PreparedStatement setRoutingKey(ByteBuffer routingKey); - - /** - * Sets the routing key for this query. - * - *

See {@link #setRoutingKey(ByteBuffer)} for more information. This method is a variant for - * when the query partition key is composite and the routing key must be built from multiple - * values. - * - * @param routingKeyComponents the raw (binary) values to compose to obtain the routing key. - * @return this {@code PreparedStatement} object. - * @see Statement#getRoutingKey - */ - public PreparedStatement setRoutingKey(ByteBuffer... routingKeyComponents); - - /** - * Returns the routing key set for this query. - * - * @return the routing key for this query or {@code null} if none has been explicitly set on this - * PreparedStatement. - */ - public ByteBuffer getRoutingKey(); - - /** - * Sets a default consistency level for all bound statements created from this prepared statement. - * - *

If no consistency level is set through this method, the bound statement created from this - * object will use the default consistency level (LOCAL_ONE). - * - *

Changing the default consistency level is not retroactive, it only applies to BoundStatement - * created after the change. - * - * @param consistency the default consistency level to set. - * @return this {@code PreparedStatement} object. - */ - public PreparedStatement setConsistencyLevel(ConsistencyLevel consistency); - - /** - * Returns the default consistency level set through {@link #setConsistencyLevel}. - * - * @return the default consistency level. Returns {@code null} if no consistency level has been - * set through this object {@code setConsistencyLevel} method. - */ - public ConsistencyLevel getConsistencyLevel(); - - /** - * Sets a default serial consistency level for all bound statements created from this prepared - * statement. - * - *

If no serial consistency level is set through this method, the bound statement created from - * this object will use the default serial consistency level (SERIAL). - * - *

Changing the default serial consistency level is not retroactive, it only applies to - * BoundStatement created after the change. - * - * @param serialConsistency the default serial consistency level to set. - * @return this {@code PreparedStatement} object. - * @throws IllegalArgumentException if {@code serialConsistency} is not one of {@code - * ConsistencyLevel.SERIAL} or {@code ConsistencyLevel.LOCAL_SERIAL}. - */ - public PreparedStatement setSerialConsistencyLevel(ConsistencyLevel serialConsistency); - - /** - * Returns the default serial consistency level set through {@link #setSerialConsistencyLevel}. - * - * @return the default serial consistency level. Returns {@code null} if no consistency level has - * been set through this object {@code setSerialConsistencyLevel} method. - */ - public ConsistencyLevel getSerialConsistencyLevel(); - - /** - * Returns the string of the query that was prepared to yield this {@code PreparedStatement}. - * - *

Note that a CQL3 query may be implicitly applied to the current keyspace (that is, if the - * keyspace is not explicitly qualified in the query itself). For prepared queries, the current - * keyspace used is the one at the time of the preparation, not the one at execution time. The - * current keyspace at the time of the preparation can be retrieved through {@link - * #getQueryKeyspace}. - * - * @return the query that was prepared to yield this {@code PreparedStatement}. - */ - public String getQueryString(); - - /** - * Returns the keyspace at the time that this prepared statement was prepared, (that is the one on - * which this statement applies unless it specified a keyspace explicitly). - * - * @return the keyspace at the time that this statement was prepared or {@code null} if no - * keyspace was set when the query was prepared (which is possible since keyspaces can be - * explicitly qualified in queries and so may not require a current keyspace to be set). - */ - public String getQueryKeyspace(); - - /** - * Convenience method to enables tracing for all bound statements created from this prepared - * statement. - * - * @return this {@code Query} object. - */ - public PreparedStatement enableTracing(); - - /** - * Convenience method to disable tracing for all bound statements created from this prepared - * statement. - * - * @return this {@code PreparedStatement} object. - */ - public PreparedStatement disableTracing(); - - /** - * Returns whether tracing is enabled for this prepared statement, i.e. if BoundStatement created - * from it will use tracing by default. - * - * @return {@code true} if this prepared statement has tracing enabled, {@code false} otherwise. - */ - public boolean isTracing(); - - /** - * Convenience method to set a default retry policy for the {@code BoundStatement} created from - * this prepared statement. - * - *

Note that this method is completely optional. By default, the retry policy used is the one - * returned {@link com.datastax.driver.core.policies.Policies#getRetryPolicy} in the cluster - * configuration. This method is only useful if you want to override this default policy for the - * {@code BoundStatement} created from this {@code PreparedStatement}. to punctually override the - * default policy for this request. - * - * @param policy the retry policy to use for this prepared statement. - * @return this {@code PreparedStatement} object. - */ - public PreparedStatement setRetryPolicy(RetryPolicy policy); - - /** - * Returns the retry policy sets for this prepared statement, if any. - * - * @return the retry policy sets specifically for this prepared statement or {@code null} if none - * have been set. - */ - public RetryPolicy getRetryPolicy(); - - /** - * Returns the prepared Id for this statement. - * - * @return the PreparedId corresponding to this statement. - */ - public PreparedId getPreparedId(); - - /** - * Return the incoming payload, that is, the payload that the server sent back with its {@code - * PREPARED} response, if any, or {@code null}, if the server did not include any custom payload. - * - *

Note that if an incoming payload is present, and if no outgoing payload has been {@link - * #setOutgoingPayload(Map) explicitly set}, then each time a {@link BoundStatement} is created - * (with either {@link #bind()} or {@link #bind(Object...)}), the resulting {@link BoundStatement} - * will inherit from this value as its default outgoing payload. - * - *

Implementors should return a read-only view of the original map, but even though, its values - * would remain inherently mutable. Callers should take care not to modify the returned map in any - * way. - * - *

This feature is only available with {@link ProtocolVersion#V4} or above; with lower - * versions, this method will always return {@code null}. - * - * @return the custom payload that the server sent back with its response, if any, or {@code - * null}, if the server did not include any custom payload. - * @since 2.2 - */ - public Map getIncomingPayload(); - - /** - * Return the outgoing payload currently associated with this statement. - * - *

If this is set to a non-null value, each time a {@link BoundStatement} is created (with - * either {@link #bind()} or {@link #bind(Object...)}), the resulting {@link BoundStatement} will - * inherit from this value as its default outgoing payload. - * - *

Implementors should return a read-only view of the original map, but even though, its values - * would remain inherently mutable. Callers should take care not to modify the returned map in any - * way. - * - *

This feature is only available with {@link ProtocolVersion#V4} or above. - * - * @return this statement's outgoing payload, if any, or {@code null} if no outgoing payload is - * set - * @since 2.2 - */ - public Map getOutgoingPayload(); - - /** - * Associate the given payload with this prepared statement. - * - *

If this is set to a non-null value, each time a {@link BoundStatement} is created (with - * either {@link #bind()} or {@link #bind(Object...)}), the resulting {@link BoundStatement} will - * inherit from this value as its default outgoing payload. - * - *

Implementors should make a defensive, thread-safe copy of the given map, but even though, - * its values would remain inherently mutable. Callers should take care not to modify the original - * map once it is passed to this method. - * - *

This feature is only available with {@link ProtocolVersion#V4} or above. Trying to include - * custom payloads in requests sent by the driver under lower protocol versions will result in an - * {@link com.datastax.driver.core.exceptions.UnsupportedFeatureException} (wrapped in a {@link - * com.datastax.driver.core.exceptions.NoHostAvailableException}). - * - * @param payload the outgoing payload to associate with this statement, or {@code null} to clear - * any previously associated payload. - * @return this {@link Statement} object. - * @since 2.2 - */ - public PreparedStatement setOutgoingPayload(Map payload); - - /** - * Return the {@link CodecRegistry} instance associated with this prepared statement. - * Implementations should never return {@code null}; instead, they should always return the {@link - * CodecRegistry} instance registered with the {@link Cluster} instance this prepared statement - * belongs to. - * - * @return the {@link CodecRegistry} instance associated with this prepared statement. - */ - public CodecRegistry getCodecRegistry(); - - /** - * Sets whether this statement is idempotent. - * - *

See {@link com.datastax.driver.core.Statement#isIdempotent} for more explanations about this - * property. - * - * @param idempotent the new value. - * @return this {@code IdempotenceAwarePreparedStatement} object. - */ - public PreparedStatement setIdempotent(Boolean idempotent); - - /** - * Whether this statement is idempotent, i.e. whether it can be applied multiple times without - * changing the result beyond the initial application. - * - *

See {@link com.datastax.driver.core.Statement#isIdempotent} for more explanations about this - * property. - * - *

Please note that idempotence will be propagated to all {@link BoundStatement}s created from - * this prepared statement. - * - * @return whether this statement is idempotent, or {@code null} to use {@link - * QueryOptions#getDefaultIdempotence()}. - */ - public Boolean isIdempotent(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ProtocolEvent.java b/driver-core/src/main/java/com/datastax/driver/core/ProtocolEvent.java deleted file mode 100644 index eb841598e3d..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ProtocolEvent.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.SchemaElement.AGGREGATE; -import static com.datastax.driver.core.SchemaElement.FUNCTION; -import static com.datastax.driver.core.SchemaElement.KEYSPACE; -import static com.datastax.driver.core.SchemaElement.TABLE; - -import io.netty.buffer.ByteBuf; -import java.net.InetSocketAddress; -import java.util.Collections; -import java.util.List; - -class ProtocolEvent { - - enum Type { - TOPOLOGY_CHANGE, - STATUS_CHANGE, - SCHEMA_CHANGE - } - - final Type type; - - private ProtocolEvent(Type type) { - this.type = type; - } - - static ProtocolEvent deserialize(ByteBuf bb, ProtocolVersion version) { - switch (CBUtil.readEnumValue(Type.class, bb)) { - case TOPOLOGY_CHANGE: - return TopologyChange.deserializeEvent(bb); - case STATUS_CHANGE: - return StatusChange.deserializeEvent(bb); - case SCHEMA_CHANGE: - return SchemaChange.deserializeEvent(bb, version); - } - throw new AssertionError(); - } - - static class TopologyChange extends ProtocolEvent { - enum Change { - NEW_NODE, - REMOVED_NODE, - MOVED_NODE - } - - final Change change; - final InetSocketAddress node; - - private TopologyChange(Change change, InetSocketAddress node) { - super(Type.TOPOLOGY_CHANGE); - this.change = change; - this.node = node; - } - - // Assumes the type has already been deserialized - private static TopologyChange deserializeEvent(ByteBuf bb) { - Change change = CBUtil.readEnumValue(Change.class, bb); - InetSocketAddress node = CBUtil.readInet(bb); - return new TopologyChange(change, node); - } - - @Override - public String toString() { - return change + " " + node; - } - } - - static class StatusChange extends ProtocolEvent { - - enum Status { - UP, - DOWN - } - - final Status status; - final InetSocketAddress node; - - private StatusChange(Status status, InetSocketAddress node) { - super(Type.STATUS_CHANGE); - this.status = status; - this.node = node; - } - - // Assumes the type has already been deserialized - private static StatusChange deserializeEvent(ByteBuf bb) { - Status status = CBUtil.readEnumValue(Status.class, bb); - InetSocketAddress node = CBUtil.readInet(bb); - return new StatusChange(status, node); - } - - @Override - public String toString() { - return status + " " + node; - } - } - - static class SchemaChange extends ProtocolEvent { - - enum Change { - CREATED, - UPDATED, - DROPPED - } - - final Change change; - final SchemaElement targetType; - final String targetKeyspace; - final String targetName; - final List targetSignature; - - SchemaChange( - Change change, - SchemaElement targetType, - String targetKeyspace, - String targetName, - List targetSignature) { - super(Type.SCHEMA_CHANGE); - this.change = change; - this.targetType = targetType; - this.targetKeyspace = targetKeyspace; - this.targetName = targetName; - this.targetSignature = targetSignature; - } - - // Assumes the type has already been deserialized - static SchemaChange deserializeEvent(ByteBuf bb, ProtocolVersion version) { - Change change; - SchemaElement targetType; - String targetKeyspace, targetName; - List targetSignature; - switch (version) { - case V1: - case V2: - change = CBUtil.readEnumValue(Change.class, bb); - targetKeyspace = CBUtil.readString(bb); - targetName = CBUtil.readString(bb); - targetType = targetName.isEmpty() ? KEYSPACE : TABLE; - targetSignature = Collections.emptyList(); - return new SchemaChange(change, targetType, targetKeyspace, targetName, targetSignature); - case V3: - case V4: - case V5: - case V6: - change = CBUtil.readEnumValue(Change.class, bb); - targetType = CBUtil.readEnumValue(SchemaElement.class, bb); - targetKeyspace = CBUtil.readString(bb); - targetName = (targetType == KEYSPACE) ? "" : CBUtil.readString(bb); - targetSignature = - (targetType == FUNCTION || targetType == AGGREGATE) - ? CBUtil.readStringList(bb) - : Collections.emptyList(); - return new SchemaChange(change, targetType, targetKeyspace, targetName, targetSignature); - default: - throw version.unsupported(); - } - } - - @Override - public String toString() { - return change.toString() - + ' ' - + targetType - + ' ' - + targetKeyspace - + (targetName.isEmpty() ? "" : '.' + targetName); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ProtocolFeature.java b/driver-core/src/main/java/com/datastax/driver/core/ProtocolFeature.java deleted file mode 100644 index fdbdbbe6ef6..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ProtocolFeature.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** A listing of features that may or not apply to a given {@link ProtocolVersion}. */ -enum ProtocolFeature { - - /** - * The capability of updating a prepared statement if the result's metadata changes at runtime - * (for example, if the query is a {@code SELECT *} and the table is altered). - */ - PREPARED_METADATA_CHANGES, - - /** The capability of sending or receiving custom payloads. */ - CUSTOM_PAYLOADS, - - /** The capability of assigning client-generated timestamps to write requests. */ - CLIENT_TIMESTAMPS, - -// -; - - /** - * Determines whether or not the input version supports ths feature. - * - * @param version the version to test against. - * @return true if supported, false otherwise. - */ - boolean isSupportedBy(ProtocolVersion version) { - switch (this) { - case PREPARED_METADATA_CHANGES: - return version.compareTo(ProtocolVersion.V5) >= 0; - case CUSTOM_PAYLOADS: - return version.compareTo(ProtocolVersion.V4) >= 0; - case CLIENT_TIMESTAMPS: - return version.compareTo(ProtocolVersion.V3) >= 0; - default: - return false; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ProtocolOptions.java b/driver-core/src/main/java/com/datastax/driver/core/ProtocolOptions.java deleted file mode 100644 index 4d4d215a049..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ProtocolOptions.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.annotations.VisibleForTesting; - -/** Options of the Cassandra native binary protocol. */ -public class ProtocolOptions { - - /** Compression supported by the Cassandra binary protocol. */ - public enum Compression { - /** No compression */ - NONE("") { - @Override - FrameCompressor compressor() { - return null; - } - }, - /** Snappy compression */ - SNAPPY("snappy") { - @Override - FrameCompressor compressor() { - return SnappyCompressor.instance; - } - }, - /** LZ4 compression */ - LZ4("lz4") { - @Override - FrameCompressor compressor() { - return LZ4Compressor.instance; - } - }; - - final String protocolName; - - private Compression(String protocolName) { - this.protocolName = protocolName; - } - - abstract FrameCompressor compressor(); - - static Compression fromString(String str) { - for (Compression c : values()) { - if (c.protocolName.equalsIgnoreCase(str)) return c; - } - return null; - } - - @Override - public String toString() { - return protocolName; - } - }; - - /** The default port for Cassandra native binary protocol: 9042. */ - public static final int DEFAULT_PORT = 9042; - - /** The default value for {@link #getMaxSchemaAgreementWaitSeconds()}: 10. */ - public static final int DEFAULT_MAX_SCHEMA_AGREEMENT_WAIT_SECONDS = 10; - - private volatile Cluster.Manager manager; - - private final int port; - final ProtocolVersion initialProtocolVersion; // What the user asked us. Will be null by default. - - @VisibleForTesting volatile int maxSchemaAgreementWaitSeconds; - - private final SSLOptions sslOptions; // null if no SSL - private final AuthProvider authProvider; - - private final boolean noCompact; - - private volatile Compression compression = Compression.NONE; - - /** - * Creates a new {@code ProtocolOptions} instance using the {@code DEFAULT_PORT} (and without - * SSL). - */ - public ProtocolOptions() { - this(DEFAULT_PORT); - } - - /** - * Creates a new {@code ProtocolOptions} instance using the provided port (without SSL nor - * authentication). - * - *

This is a shortcut for {@code new ProtocolOptions(port, null, AuthProvider.NONE)}. - * - * @param port the port to use for the binary protocol. - */ - public ProtocolOptions(int port) { - this(port, null, DEFAULT_MAX_SCHEMA_AGREEMENT_WAIT_SECONDS, null, AuthProvider.NONE, false); - } - - /** - * Creates a new {@code ProtocolOptions} instance using the provided port and SSL context. - * - * @param port the port to use for the binary protocol. - * @param protocolVersion the protocol version to use. This can be {@code null}, in which case the - * version used will be the biggest version supported by the first node the driver - * connects to. See {@link Cluster.Builder#withProtocolVersion} for more details. - * @param sslOptions the SSL options to use. Use {@code null} if SSL is not to be used. - * @param authProvider the {@code AuthProvider} to use for authentication against the Cassandra - * nodes. - */ - public ProtocolOptions( - int port, - ProtocolVersion protocolVersion, - int maxSchemaAgreementWaitSeconds, - SSLOptions sslOptions, - AuthProvider authProvider) { - this(port, protocolVersion, maxSchemaAgreementWaitSeconds, sslOptions, authProvider, false); - } - - /** - * Creates a new {@code ProtocolOptions} instance using the provided port and SSL context. - * - * @param port the port to use for the binary protocol. - * @param protocolVersion the protocol version to use. This can be {@code null}, in which case the - * version used will be the biggest version supported by the first node the driver - * connects to. See {@link Cluster.Builder#withProtocolVersion} for more details. - * @param sslOptions the SSL options to use. Use {@code null} if SSL is not to be used. - * @param authProvider the {@code AuthProvider} to use for authentication against the Cassandra - * nodes. - * @param noCompact whether or not to include the NO_COMPACT startup option. - */ - public ProtocolOptions( - int port, - ProtocolVersion protocolVersion, - int maxSchemaAgreementWaitSeconds, - SSLOptions sslOptions, - AuthProvider authProvider, - boolean noCompact) { - this.port = port; - this.initialProtocolVersion = protocolVersion; - this.maxSchemaAgreementWaitSeconds = maxSchemaAgreementWaitSeconds; - this.sslOptions = sslOptions; - this.authProvider = authProvider; - this.noCompact = noCompact; - } - - void register(Cluster.Manager manager) { - this.manager = manager; - } - - /** - * Returns the port used to connect to the Cassandra hosts. - * - * @return the port used to connect to the Cassandra hosts. - */ - public int getPort() { - return port; - } - - /** - * The protocol version used by the Cluster instance. - * - * @return the protocol version in use. This might return {@code null} if a particular version - * hasn't been forced by the user (using say {Cluster.Builder#withProtocolVersion}) - * and this Cluster instance has not yet connected to any node (but as soon as the - * Cluster instance is connected, this is guaranteed to return a non-null value). Note that - * nodes that do not support this protocol version will be ignored. - */ - public ProtocolVersion getProtocolVersion() { - return manager == null || manager.connectionFactory == null - ? null - : manager.connectionFactory.protocolVersion; - } - - /** - * Returns the compression used by the protocol. - * - *

By default, compression is not used. - * - * @return the compression used. - */ - public Compression getCompression() { - return compression; - } - - /** - * Sets the compression to use. - * - *

Note that while this setting can be changed at any time, it will only apply to newly created - * connections. - * - * @param compression the compression algorithm to use (or {@code Compression.NONE} to disable - * compression). - * @return this {@code ProtocolOptions} object. - * @throws IllegalStateException if the compression requested is not available. Most compression - * algorithms require that the relevant be present in the classpath. If not, the compression - * will be unavailable. - */ - public ProtocolOptions setCompression(Compression compression) { - if (compression != Compression.NONE && compression.compressor() == null) - throw new IllegalStateException( - "The requested compression is not available (some compression require a JAR to be found in the classpath)"); - - this.compression = compression; - return this; - } - - /** - * Returns the maximum time to wait for schema agreement before returning from a DDL query. - * - * @return the time. - */ - public int getMaxSchemaAgreementWaitSeconds() { - return maxSchemaAgreementWaitSeconds; - } - - /** - * The {@code SSLOptions} used by this cluster. - * - * @return the {@code SSLOptions} used by this cluster (set at the cluster creation time) or - * {@code null} if SSL is not in use. - */ - public SSLOptions getSSLOptions() { - return sslOptions; - } - - /** - * The {@code AuthProvider} used by this cluster. - * - * @return the {@code AuthProvided} used by this cluster (set at the cluster creation time). If no - * authentication mechanism is in use (the default), {@code AuthProvided.NONE} will be - * returned. - */ - public AuthProvider getAuthProvider() { - return authProvider; - } - - /** @return Whether or not to include the NO_COMPACT startup option. */ - public boolean isNoCompact() { - return noCompact; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ProtocolV1Authenticator.java b/driver-core/src/main/java/com/datastax/driver/core/ProtocolV1Authenticator.java deleted file mode 100644 index a5ee8009190..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ProtocolV1Authenticator.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.util.Map; - -/** - * Parent class for {@link Authenticator} implementations that support native protocol v1 - * authentication. - * - *

Protocol v1 uses simple, credentials-based authentication (as opposed to SASL for later - * protocol versions). In order to support protocol v1, an authenticator must extend this class. - * - *

We use an abstract class instead of an interface because we don't want to expose {@link - * #getCredentials()}. - * - * @see Native - * protocol v1 specification - */ -abstract class ProtocolV1Authenticator { - abstract Map getCredentials(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ProtocolVersion.java b/driver-core/src/main/java/com/datastax/driver/core/ProtocolVersion.java deleted file mode 100644 index fb0ad0bcff9..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ProtocolVersion.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableMap.Builder; -import java.util.Map; - -/** Versions of the native protocol supported by the driver. */ -public enum ProtocolVersion { - V1("1.2.0", 1, null), - V2("2.0.0", 2, V1), - V3("2.1.0", 3, V2), - V4("2.2.0", 4, V3), - V5("4.0.0", 5, V4), - V6("4.0.0", 6, V5); - - /** The most recent protocol version supported by the driver. */ - public static final ProtocolVersion NEWEST_SUPPORTED = V5; - - /** The most recent beta protocol version supported by the driver. */ - public static final ProtocolVersion NEWEST_BETA = V6; - - private final VersionNumber minCassandraVersion; - - private final int asInt; - - private final ProtocolVersion lowerSupported; - - private ProtocolVersion(String minCassandraVersion, int asInt, ProtocolVersion lowerSupported) { - this.minCassandraVersion = VersionNumber.parse(minCassandraVersion); - this.asInt = asInt; - this.lowerSupported = lowerSupported; - } - - VersionNumber minCassandraVersion() { - return minCassandraVersion; - } - - DriverInternalError unsupported() { - return new DriverInternalError("Unsupported protocol version " + this); - } - - /** - * Returns the version as an integer. - * - * @return the integer representation. - */ - public int toInt() { - return asInt; - } - - /** - * Returns the highest supported version that is lower than this version. Returns {@code null} if - * there isn't such a version. - * - * @return the highest supported version that is lower than this version. - */ - public ProtocolVersion getLowerSupported() { - return lowerSupported; - } - - private static final Map INT_TO_VERSION; - - static { - Builder builder = ImmutableMap.builder(); - for (ProtocolVersion version : values()) { - builder.put(version.asInt, version); - } - INT_TO_VERSION = builder.build(); - } - - /** - * Returns the value matching an integer version. - * - * @param i the version as an integer. - * @return the matching enum value. - * @throws IllegalArgumentException if the argument doesn't match any known version. - */ - public static ProtocolVersion fromInt(int i) { - ProtocolVersion version = INT_TO_VERSION.get(i); - if (version == null) - throw new IllegalArgumentException("No protocol version matching integer version " + i); - return version; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/QueryLogger.java b/driver-core/src/main/java/com/datastax/driver/core/QueryLogger.java deleted file mode 100644 index 591320533ad..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/QueryLogger.java +++ /dev/null @@ -1,970 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static java.util.concurrent.TimeUnit.NANOSECONDS; - -import com.datastax.driver.core.querybuilder.BuiltStatement; -import com.google.common.annotations.VisibleForTesting; -import java.nio.ByteBuffer; -import java.util.Iterator; -import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A configurable {@link LatencyTracker} that logs all executed statements. - * - *

Typically, client applications would instantiate one single query logger (using its {@link - * Builder}), configure it and register it on the relevant {@link Cluster} instance, e.g.: - * - *

- * - *

- * Cluster cluster = ...
- * QueryLogger queryLogger = QueryLogger.builder()
- *     .withConstantThreshold(...)
- *     .withMaxQueryStringLength(...)
- *     .build();
- * cluster.register(queryLogger);
- * 
- * - *

Refer to the {@link Builder} documentation for more information on configuration settings for - * the query logger. - * - *

Once registered, the query logger will log every {@link RegularStatement}, {@link - * BoundStatement} or {@link BatchStatement} executed by the driver; note that it will never log - * other types of statement, null statements nor any special statement used internally by the - * driver. - * - *

There is one log for each request to a Cassandra node; because the driver sometimes retries - * the same statement on multiple nodes, a single statement execution (for example, a single call to - * {@link Session#execute(Statement)}) can produce multiple logs on different nodes. - * - *

For more flexibility, the query logger uses 3 different {@link Logger} instances: - * - *

- * - *

    - *
  1. {@link #NORMAL_LOGGER}: used to log normal queries, i.e., queries that completed - * successfully within a configurable threshold in milliseconds. - *
  2. {@link #SLOW_LOGGER}: used to log slow queries, i.e., queries that completed successfully - * but that took longer than a configurable threshold in milliseconds to complete. - *
  3. {@link #ERROR_LOGGER}: used to log unsuccessful queries, i.e., queries that did not - * completed normally and threw an exception. Note this this logger will also print the full - * stack trace of the reported exception. - *
- * - *

- * - *

The appropriate logger is chosen according to the following algorithm: - * - *

    - *
  1. if an exception has been thrown: use {@link #ERROR_LOGGER}; - *
  2. otherwise, if the reported latency is greater than the configured threshold in - * milliseconds: use {@link #SLOW_LOGGER}; - *
  3. otherwise, use {@link #NORMAL_LOGGER}. - *
- * - *

- * - *

All loggers are activated by setting their levels to {@code DEBUG} or {@code TRACE} (including - * {@link #ERROR_LOGGER}). If the level is set to {@code TRACE} and the statement being logged is a - * {@link BoundStatement}, then the query parameters (if any) will be logged as well (names and - * actual values). - * - *

- * - *

Constant thresholds vs. Dynamic thresholds - * - *

Currently the QueryLogger can track slow queries in two different ways: using a {@link - * Builder#withConstantThreshold(long)} constant threshold} in milliseconds (which is the default - * behavior), or using a {@link Builder#withDynamicThreshold(PercentileTracker, double) dynamic - * threshold} based on latency percentiles. - * - *

This class is thread-safe. - * - * @since 2.0.10 - */ -public abstract class QueryLogger implements LatencyTracker { - - /** - * The default latency threshold in milliseconds beyond which queries are considered 'slow' and - * logged as such by the driver. - */ - public static final long DEFAULT_SLOW_QUERY_THRESHOLD_MS = 5000; - - /** - * The default latency percentile beyond which queries are considered 'slow' and logged as such by - * the driver. - */ - public static final double DEFAULT_SLOW_QUERY_THRESHOLD_PERCENTILE = 99.0; - - /** - * The default maximum length of a CQL query string that can be logged verbatim by the driver. - * Query strings longer than this value will be truncated when logged. - */ - public static final int DEFAULT_MAX_QUERY_STRING_LENGTH = 500; - - /** - * The default maximum length of a query parameter value that can be logged verbatim by the - * driver. Parameter values longer than this value will be truncated when logged. - */ - public static final int DEFAULT_MAX_PARAMETER_VALUE_LENGTH = 50; - - /** - * The default maximum number of query parameters that can be logged by the driver. Queries with a - * number of parameters higher than this value will not have all their parameters logged. - */ - public static final int DEFAULT_MAX_LOGGED_PARAMETERS = 50; - - // Loggers - - /** - * The logger used to log normal queries, i.e., queries that completed successfully within a - * configurable threshold in milliseconds. - * - *

This logger is activated by setting its level to {@code DEBUG} or {@code TRACE}. - * Additionally, if the level is set to {@code TRACE} and the statement being logged is a {@link - * BoundStatement} or a {@link SimpleStatement}, then the query parameters (if any) will be - * logged. For a {@link BoundStatement} names and actual values are logged and for a {@link - * SimpleStatement} values are logged in positional order and named values are logged with names - * and value. - * - *

The name of this logger is {@code com.datastax.driver.core.QueryLogger.NORMAL}. - */ - public static final Logger NORMAL_LOGGER = - LoggerFactory.getLogger("com.datastax.driver.core.QueryLogger.NORMAL"); - - /** - * The logger used to log slow queries, i.e., queries that completed successfully but whose - * execution time exceeded a configurable threshold in milliseconds. - * - *

This logger is activated by setting its level to {@code DEBUG} or {@code TRACE}. - * Additionally, if the level is set to {@code TRACE} and the statement being logged is a {@link - * BoundStatement} or a {@link SimpleStatement}, then the query parameters (if any) will be - * logged. For a {@link BoundStatement} names and actual values are logged and for a {@link - * SimpleStatement} values are logged in positional order and named values are logged with names - * and value. - * - *

The name of this logger is {@code com.datastax.driver.core.QueryLogger.SLOW}. - */ - public static final Logger SLOW_LOGGER = - LoggerFactory.getLogger("com.datastax.driver.core.QueryLogger.SLOW"); - - /** - * The logger used to log unsuccessful queries, i.e., queries that did not complete normally and - * threw an exception. - * - *

This logger is activated by setting its level to {@code DEBUG} or {@code TRACE}. - * Additionally, if the level is set to {@code TRACE} and the statement being logged is a {@link - * BoundStatement} or a {@link SimpleStatement}, then the query parameters (if any) will be - * logged. For a {@link BoundStatement} names and actual values are logged and for a {@link - * SimpleStatement} values are logged in positional order and named values are logged with names - * and value. Note this this logger will also print the full stack trace of the reported - * exception. - * - *

The name of this logger is {@code com.datastax.driver.core.QueryLogger.ERROR}. - */ - public static final Logger ERROR_LOGGER = - LoggerFactory.getLogger("com.datastax.driver.core.QueryLogger.ERROR"); - - // Message templates - - private static final String NORMAL_TEMPLATE = - "[%s] [%s] Query completed normally, took %s ms: %s"; - - private static final String SLOW_TEMPLATE_MILLIS = "[%s] [%s] Query too slow, took %s ms: %s"; - - private static final String SLOW_TEMPLATE_PERCENTILE = - "[%s] [%s] Query too slow, took %s ms (%s percentile = %s ms): %s"; - - private static final String ERROR_TEMPLATE = "[%s] [%s] Query error after %s ms: %s"; - - @VisibleForTesting static final String TRUNCATED_OUTPUT = "... [truncated output]"; - - @VisibleForTesting static final String FURTHER_PARAMS_OMITTED = " [further parameters omitted]"; - - protected volatile Cluster cluster; - - private volatile ProtocolVersion protocolVersion; - - protected volatile int maxQueryStringLength; - - protected volatile int maxParameterValueLength; - - protected volatile int maxLoggedParameters; - - /** - * Private constructor. Instances of QueryLogger should be obtained via the {@link #builder()} - * method. - */ - private QueryLogger( - int maxQueryStringLength, int maxParameterValueLength, int maxLoggedParameters) { - this.maxQueryStringLength = maxQueryStringLength; - this.maxParameterValueLength = maxParameterValueLength; - this.maxLoggedParameters = maxLoggedParameters; - } - - /** - * Creates a new {@link QueryLogger.Builder} instance. - * - *

This is a convenience method for {@code new QueryLogger.Builder()}. - * - * @return the new QueryLogger builder. - * @throws NullPointerException if {@code cluster} is {@code null}. - */ - public static QueryLogger.Builder builder() { - return new QueryLogger.Builder(); - } - - @Override - public void onRegister(Cluster cluster) { - this.cluster = cluster; - } - - @Override - public void onUnregister(Cluster cluster) { - // nothing to do - } - - /** - * A QueryLogger that uses a constant threshold in milliseconds to track slow queries. This - * implementation is the default and should be preferred to {@link DynamicThresholdQueryLogger} - * which is still in beta state. - */ - public static class ConstantThresholdQueryLogger extends QueryLogger { - - private volatile long slowQueryLatencyThresholdMillis; - - private ConstantThresholdQueryLogger( - int maxQueryStringLength, - int maxParameterValueLength, - int maxLoggedParameters, - long slowQueryLatencyThresholdMillis) { - super(maxQueryStringLength, maxParameterValueLength, maxLoggedParameters); - this.setSlowQueryLatencyThresholdMillis(slowQueryLatencyThresholdMillis); - } - - /** - * Return the threshold in milliseconds beyond which queries are considered 'slow' and logged as - * such by the driver. The default value is {@link #DEFAULT_SLOW_QUERY_THRESHOLD_MS}. - * - * @return The threshold in milliseconds beyond which queries are considered 'slow' and logged - * as such by the driver. - */ - public long getSlowQueryLatencyThresholdMillis() { - return slowQueryLatencyThresholdMillis; - } - - /** - * Set the threshold in milliseconds beyond which queries are considered 'slow' and logged as - * such by the driver. - * - * @param slowQueryLatencyThresholdMillis Slow queries threshold in milliseconds. It must be - * strictly positive. - * @throws IllegalArgumentException if {@code slowQueryLatencyThresholdMillis <= 0}. - */ - public void setSlowQueryLatencyThresholdMillis(long slowQueryLatencyThresholdMillis) { - if (slowQueryLatencyThresholdMillis <= 0) - throw new IllegalArgumentException( - "Invalid slowQueryLatencyThresholdMillis, should be > 0, got " - + slowQueryLatencyThresholdMillis); - this.slowQueryLatencyThresholdMillis = slowQueryLatencyThresholdMillis; - } - - @Override - protected void maybeLogNormalOrSlowQuery(Host host, Statement statement, long latencyMs) { - if (latencyMs > slowQueryLatencyThresholdMillis) { - maybeLogSlowQuery(host, statement, latencyMs); - } else { - maybeLogNormalQuery(host, statement, latencyMs); - } - } - - protected void maybeLogSlowQuery(Host host, Statement statement, long latencyMs) { - if (SLOW_LOGGER.isDebugEnabled()) { - String message = - String.format( - SLOW_TEMPLATE_MILLIS, - cluster.getClusterName(), - host, - latencyMs, - statementAsString(statement)); - logQuery(statement, null, SLOW_LOGGER, message); - } - } - } - - /** - * A QueryLogger that uses a dynamic threshold in milliseconds to track slow queries. - * - *

Dynamic thresholds are based on per-host latency percentiles, as computed by {@link - * PercentileTracker}. - */ - public static class DynamicThresholdQueryLogger extends QueryLogger { - - private volatile double slowQueryLatencyThresholdPercentile; - - private volatile PercentileTracker percentileLatencyTracker; - - private DynamicThresholdQueryLogger( - int maxQueryStringLength, - int maxParameterValueLength, - int maxLoggedParameters, - double slowQueryLatencyThresholdPercentile, - PercentileTracker percentileLatencyTracker) { - super(maxQueryStringLength, maxParameterValueLength, maxLoggedParameters); - this.setSlowQueryLatencyThresholdPercentile(slowQueryLatencyThresholdPercentile); - this.setPercentileLatencyTracker(percentileLatencyTracker); - } - - /** - * Return the percentile tracker to use for recording per-host latency histograms. Cannot be - * {@code null}. - * - * @return the percentile tracker to use. - */ - public PercentileTracker getPercentileLatencyTracker() { - return percentileLatencyTracker; - } - - /** - * Set the percentile tracker to use for recording per-host latency histograms. Cannot be {@code - * null}. - * - * @param percentileLatencyTracker the percentile tracker instance to use. - * @throws IllegalArgumentException if {@code percentileLatencyTracker == null}. - */ - public void setPercentileLatencyTracker(PercentileTracker percentileLatencyTracker) { - if (percentileLatencyTracker == null) - throw new IllegalArgumentException("perHostPercentileLatencyTracker cannot be null"); - this.percentileLatencyTracker = percentileLatencyTracker; - } - - /** - * Return the threshold percentile beyond which queries are considered 'slow' and logged as such - * by the driver. The default value is {@link #DEFAULT_SLOW_QUERY_THRESHOLD_PERCENTILE}. - * - * @return threshold percentile beyond which queries are considered 'slow' and logged as such by - * the driver. - */ - public double getSlowQueryLatencyThresholdPercentile() { - return slowQueryLatencyThresholdPercentile; - } - - /** - * Set the threshold percentile beyond which queries are considered 'slow' and logged as such by - * the driver. - * - * @param slowQueryLatencyThresholdPercentile Slow queries threshold percentile. It must be - * comprised between 0 inclusive and 100 exclusive. - * @throws IllegalArgumentException if {@code slowQueryLatencyThresholdPercentile < 0 || - * slowQueryLatencyThresholdPercentile >= 100}. - */ - public void setSlowQueryLatencyThresholdPercentile(double slowQueryLatencyThresholdPercentile) { - if (slowQueryLatencyThresholdPercentile < 0.0 || slowQueryLatencyThresholdPercentile >= 100.0) - throw new IllegalArgumentException( - "Invalid slowQueryLatencyThresholdPercentile, should be >= 0 and < 100, got " - + slowQueryLatencyThresholdPercentile); - this.slowQueryLatencyThresholdPercentile = slowQueryLatencyThresholdPercentile; - } - - @Override - protected void maybeLogNormalOrSlowQuery(Host host, Statement statement, long latencyMs) { - long threshold = - percentileLatencyTracker.getLatencyAtPercentile( - host, statement, null, slowQueryLatencyThresholdPercentile); - if (threshold >= 0 && latencyMs > threshold) { - maybeLogSlowQuery(host, statement, latencyMs, threshold); - } else { - maybeLogNormalQuery(host, statement, latencyMs); - } - } - - protected void maybeLogSlowQuery( - Host host, Statement statement, long latencyMs, long threshold) { - if (SLOW_LOGGER.isDebugEnabled()) { - String message = - String.format( - SLOW_TEMPLATE_PERCENTILE, - cluster.getClusterName(), - host, - latencyMs, - slowQueryLatencyThresholdPercentile, - threshold, - statementAsString(statement)); - logQuery(statement, null, SLOW_LOGGER, message); - } - } - - @Override - public void onRegister(Cluster cluster) { - super.onRegister(cluster); - cluster.register(percentileLatencyTracker); - } - - // Don't unregister the latency tracker in onUnregister, we can't guess if it's being used by - // another component - // or not. - } - - /** Helper class to build {@link QueryLogger} instances with a fluent API. */ - public static class Builder { - - private int maxQueryStringLength = DEFAULT_MAX_QUERY_STRING_LENGTH; - - private int maxParameterValueLength = DEFAULT_MAX_PARAMETER_VALUE_LENGTH; - - private int maxLoggedParameters = DEFAULT_MAX_LOGGED_PARAMETERS; - - private long slowQueryLatencyThresholdMillis = DEFAULT_SLOW_QUERY_THRESHOLD_MS; - - private double slowQueryLatencyThresholdPercentile = DEFAULT_SLOW_QUERY_THRESHOLD_PERCENTILE; - - private PercentileTracker percentileLatencyTracker; - - private boolean constantThreshold = true; - - /** - * Enables slow query latency tracking based on constant thresholds. - * - *

Note: You should either use {@link #withConstantThreshold(long) constant thresholds} or - * {@link #withDynamicThreshold(PercentileTracker, double) dynamic thresholds}, not both. - * - * @param slowQueryLatencyThresholdMillis The threshold in milliseconds beyond which queries are - * considered 'slow' and logged as such by the driver. The default value is {@link - * #DEFAULT_SLOW_QUERY_THRESHOLD_MS} - * @return this {@link Builder} instance (for method chaining). - */ - public Builder withConstantThreshold(long slowQueryLatencyThresholdMillis) { - this.slowQueryLatencyThresholdMillis = slowQueryLatencyThresholdMillis; - constantThreshold = true; - return this; - } - - /** - * Enables slow query latency tracking based on dynamic thresholds. - * - *

Dynamic thresholds are based on latency percentiles, as computed by {@link - * PercentileTracker}. - * - *

Note: You should either use {@link #withConstantThreshold(long) constant thresholds} or - * {@link #withDynamicThreshold(PercentileTracker, double) dynamic thresholds}, not both. - * - * @param percentileLatencyTracker the {@link PercentileTracker} instance to use for recording - * latency histograms. Cannot be {@code null}. It will get {@link - * Cluster#register(LatencyTracker) registered} with the cluster at the same time as this - * logger. - * @param slowQueryLatencyThresholdPercentile Slow queries threshold percentile. It must be - * comprised between 0 inclusive and 100 exclusive. The default value is {@link - * #DEFAULT_SLOW_QUERY_THRESHOLD_PERCENTILE} - * @return this {@link Builder} instance (for method chaining). - */ - public Builder withDynamicThreshold( - PercentileTracker percentileLatencyTracker, double slowQueryLatencyThresholdPercentile) { - this.percentileLatencyTracker = percentileLatencyTracker; - this.slowQueryLatencyThresholdPercentile = slowQueryLatencyThresholdPercentile; - constantThreshold = false; - return this; - } - - /** - * Set the maximum length of a CQL query string that can be logged verbatim by the driver. Query - * strings longer than this value will be truncated when logged. - * - * @param maxQueryStringLength The maximum length of a CQL query string that can be logged - * verbatim by the driver. It must be strictly positive or {@code -1}, in which case the - * query is never truncated (use with care). The default value is {@link - * #DEFAULT_MAX_QUERY_STRING_LENGTH}. - * @return this {@link Builder} instance (for method chaining). - */ - public Builder withMaxQueryStringLength(int maxQueryStringLength) { - this.maxQueryStringLength = maxQueryStringLength; - return this; - } - - /** - * Set the maximum length of a query parameter value that can be logged verbatim by the driver. - * Parameter values longer than this value will be truncated when logged. - * - * @param maxParameterValueLength The maximum length of a query parameter value that can be - * logged verbatim by the driver. It must be strictly positive or {@code -1}, in which case - * the parameter value is never truncated (use with care). The default value is {@link - * #DEFAULT_MAX_PARAMETER_VALUE_LENGTH}. - * @return this {@link Builder} instance (for method chaining). - */ - public Builder withMaxParameterValueLength(int maxParameterValueLength) { - this.maxParameterValueLength = maxParameterValueLength; - return this; - } - - /** - * Set the maximum number of query parameters that can be logged by the driver. Queries with a - * number of parameters higher than this value will not have all their parameters logged. - * - * @param maxLoggedParameters The maximum number of query parameters that can be logged by the - * driver. It must be strictly positive or {@code -1}, in which case all parameters will be - * logged, regardless of their number (use with care). The default value is {@link - * #DEFAULT_MAX_LOGGED_PARAMETERS}. - * @return this {@link Builder} instance (for method chaining). - */ - public Builder withMaxLoggedParameters(int maxLoggedParameters) { - this.maxLoggedParameters = maxLoggedParameters; - return this; - } - - /** - * Build the {@link QueryLogger} instance. - * - * @return the {@link QueryLogger} instance. - * @throws IllegalArgumentException if the builder is unable to build a valid instance due to - * incorrect settings. - */ - public QueryLogger build() { - if (constantThreshold) { - return new ConstantThresholdQueryLogger( - maxQueryStringLength, - maxParameterValueLength, - maxLoggedParameters, - slowQueryLatencyThresholdMillis); - } else { - return new DynamicThresholdQueryLogger( - maxQueryStringLength, - maxParameterValueLength, - maxLoggedParameters, - slowQueryLatencyThresholdPercentile, - percentileLatencyTracker); - } - } - } - - // Getters and Setters - - /** - * Return the maximum length of a CQL query string that can be logged verbatim by the driver. - * Query strings longer than this value will be truncated when logged. The default value is {@link - * #DEFAULT_MAX_QUERY_STRING_LENGTH}. - * - * @return The maximum length of a CQL query string that can be logged verbatim by the driver. - */ - public int getMaxQueryStringLength() { - return maxQueryStringLength; - } - - /** - * Set the maximum length of a CQL query string that can be logged verbatim by the driver. Query - * strings longer than this value will be truncated when logged. - * - * @param maxQueryStringLength The maximum length of a CQL query string that can be logged - * verbatim by the driver. It must be strictly positive or {@code -1}, in which case the query - * is never truncated (use with care). - * @throws IllegalArgumentException if {@code maxQueryStringLength <= 0 && maxQueryStringLength != - * -1}. - */ - public void setMaxQueryStringLength(int maxQueryStringLength) { - if (maxQueryStringLength <= 0 && maxQueryStringLength != -1) - throw new IllegalArgumentException( - "Invalid maxQueryStringLength, should be > 0 or -1, got " + maxQueryStringLength); - this.maxQueryStringLength = maxQueryStringLength; - } - - /** - * Return the maximum length of a query parameter value that can be logged verbatim by the driver. - * Parameter values longer than this value will be truncated when logged. The default value is - * {@link #DEFAULT_MAX_PARAMETER_VALUE_LENGTH}. - * - * @return The maximum length of a query parameter value that can be logged verbatim by the - * driver. - */ - public int getMaxParameterValueLength() { - return maxParameterValueLength; - } - - /** - * Set the maximum length of a query parameter value that can be logged verbatim by the driver. - * Parameter values longer than this value will be truncated when logged. - * - * @param maxParameterValueLength The maximum length of a query parameter value that can be logged - * verbatim by the driver. It must be strictly positive or {@code -1}, in which case the - * parameter value is never truncated (use with care). - * @throws IllegalArgumentException if {@code maxParameterValueLength <= 0 && - * maxParameterValueLength != -1}. - */ - public void setMaxParameterValueLength(int maxParameterValueLength) { - if (maxParameterValueLength <= 0 && maxParameterValueLength != -1) - throw new IllegalArgumentException( - "Invalid maxParameterValueLength, should be > 0 or -1, got " + maxParameterValueLength); - this.maxParameterValueLength = maxParameterValueLength; - } - - /** - * Return the maximum number of query parameters that can be logged by the driver. Queries with a - * number of parameters higher than this value will not have all their parameters logged. The - * default value is {@link #DEFAULT_MAX_LOGGED_PARAMETERS}. - * - * @return The maximum number of query parameters that can be logged by the driver. - */ - public int getMaxLoggedParameters() { - return maxLoggedParameters; - } - - /** - * Set the maximum number of query parameters that can be logged by the driver. Queries with a - * number of parameters higher than this value will not have all their parameters logged. - * - * @param maxLoggedParameters the maximum number of query parameters that can be logged by the - * driver. It must be strictly positive or {@code -1}, in which case all parameters will be - * logged, regardless of their number (use with care). - * @throws IllegalArgumentException if {@code maxLoggedParameters <= 0 && maxLoggedParameters != - * -1}. - */ - public void setMaxLoggedParameters(int maxLoggedParameters) { - if (maxLoggedParameters <= 0 && maxLoggedParameters != -1) - throw new IllegalArgumentException( - "Invalid maxLoggedParameters, should be > 0 or -1, got " + maxLoggedParameters); - this.maxLoggedParameters = maxLoggedParameters; - } - - /** {@inheritDoc} */ - @Override - public void update(Host host, Statement statement, Exception exception, long newLatencyNanos) { - if (cluster == null) - throw new IllegalStateException( - "This method should only be called after the logger has been registered with a cluster"); - - if (statement instanceof StatementWrapper) - statement = ((StatementWrapper) statement).getWrappedStatement(); - - long latencyMs = NANOSECONDS.toMillis(newLatencyNanos); - if (exception == null) { - maybeLogNormalOrSlowQuery(host, statement, latencyMs); - } else { - maybeLogErrorQuery(host, statement, exception, latencyMs); - } - } - - protected abstract void maybeLogNormalOrSlowQuery(Host host, Statement statement, long latencyMs); - - protected void maybeLogNormalQuery(Host host, Statement statement, long latencyMs) { - if (NORMAL_LOGGER.isDebugEnabled()) { - String message = - String.format( - NORMAL_TEMPLATE, - cluster.getClusterName(), - host, - latencyMs, - statementAsString(statement)); - logQuery(statement, null, NORMAL_LOGGER, message); - } - } - - protected void maybeLogErrorQuery( - Host host, Statement statement, Exception exception, long latencyMs) { - if (ERROR_LOGGER.isDebugEnabled() - && !(exception instanceof CancelledSpeculativeExecutionException)) { - String message = - String.format( - ERROR_TEMPLATE, - cluster.getClusterName(), - host, - latencyMs, - statementAsString(statement)); - logQuery(statement, exception, ERROR_LOGGER, message); - } - } - - protected void logQuery(Statement statement, Exception exception, Logger logger, String message) { - boolean showParameterValues = logger.isTraceEnabled(); - if (showParameterValues) { - StringBuilder params = new StringBuilder(); - if (statement instanceof BoundStatement) { - appendParameters((BoundStatement) statement, params, maxLoggedParameters); - } else if (statement instanceof SimpleStatement) { - appendParameters((SimpleStatement) statement, params, maxLoggedParameters); - } else if (statement instanceof BatchStatement) { - BatchStatement batchStatement = (BatchStatement) statement; - int remaining = maxLoggedParameters; - for (Statement inner : batchStatement.getStatements()) { - if (inner instanceof BoundStatement) { - remaining = appendParameters((BoundStatement) inner, params, remaining); - } else if (inner instanceof SimpleStatement) { - remaining = appendParameters((SimpleStatement) inner, params, remaining); - } - } - } else if (statement instanceof BuiltStatement) { - appendParameters((BuiltStatement) statement, params, maxLoggedParameters); - } - if (params.length() > 0) params.append("]"); - logger.trace(message + params, exception); - } else { - logger.debug(message, exception); - } - } - - protected String statementAsString(Statement statement) { - StringBuilder sb = new StringBuilder(); - if (statement instanceof BatchStatement) { - BatchStatement bs = (BatchStatement) statement; - int statements = bs.getStatements().size(); - int boundValues = countBoundValues(bs); - sb.append("[" + statements + " statements, " + boundValues + " bound values] "); - } else if (statement instanceof BoundStatement) { - int boundValues = ((BoundStatement) statement).wrapper.values.length; - sb.append("[" + boundValues + " bound values] "); - } else if (statement instanceof SimpleStatement) { - int boundValues = ((SimpleStatement) statement).valuesCount(); - sb.append("[" + boundValues + " bound values] "); - } - - append(statement, sb, maxQueryStringLength); - return sb.toString(); - } - - protected int countBoundValues(BatchStatement bs) { - int count = 0; - for (Statement s : bs.getStatements()) { - if (s instanceof BoundStatement) count += ((BoundStatement) s).wrapper.values.length; - else if (s instanceof SimpleStatement) count += ((SimpleStatement) s).valuesCount(); - } - return count; - } - - protected int appendParameters(BoundStatement statement, StringBuilder buffer, int remaining) { - if (remaining == 0) return 0; - ColumnDefinitions metadata = statement.preparedStatement().getVariables(); - int numberOfParameters = metadata.size(); - if (numberOfParameters > 0) { - List definitions = metadata.asList(); - int numberOfLoggedParameters; - if (remaining == -1) { - numberOfLoggedParameters = numberOfParameters; - } else { - numberOfLoggedParameters = Math.min(remaining, numberOfParameters); - remaining -= numberOfLoggedParameters; - } - for (int i = 0; i < numberOfLoggedParameters; i++) { - if (buffer.length() == 0) buffer.append(" ["); - else buffer.append(", "); - String value = - statement.isSet(i) - ? parameterValueAsString(definitions.get(i), statement.wrapper.values[i]) - : ""; - buffer.append(String.format("%s:%s", metadata.getName(i), value)); - } - if (numberOfLoggedParameters < numberOfParameters) { - buffer.append(FURTHER_PARAMS_OMITTED); - } - } - return remaining; - } - - protected String parameterValueAsString(ColumnDefinitions.Definition definition, ByteBuffer raw) { - String valueStr; - if (raw == null || raw.remaining() == 0) { - valueStr = "NULL"; - } else { - DataType type = definition.getType(); - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - TypeCodec codec = codecRegistry.codecFor(type); - int maxParameterValueLength = this.maxParameterValueLength; - if (type.equals(DataType.blob()) && maxParameterValueLength != -1) { - // prevent large blobs from being converted to strings - int maxBufferLength = Math.max(2, (maxParameterValueLength - 2) / 2); - boolean bufferTooLarge = raw.remaining() > maxBufferLength; - if (bufferTooLarge) { - raw = (ByteBuffer) raw.duplicate().limit(maxBufferLength); - } - Object value = codec.deserialize(raw, protocolVersion()); - valueStr = codec.format(value); - if (bufferTooLarge) { - valueStr = valueStr + TRUNCATED_OUTPUT; - } - } else { - Object value = codec.deserialize(raw, protocolVersion()); - valueStr = codec.format(value); - if (maxParameterValueLength != -1 && valueStr.length() > maxParameterValueLength) { - valueStr = valueStr.substring(0, maxParameterValueLength) + TRUNCATED_OUTPUT; - } - } - } - return valueStr; - } - - protected int appendParameters(SimpleStatement statement, StringBuilder buffer, int remaining) { - if (remaining == 0) return 0; - int numberOfParameters = statement.valuesCount(); - if (numberOfParameters > 0) { - int numberOfLoggedParameters; - if (remaining == -1) { - numberOfLoggedParameters = numberOfParameters; - } else { - numberOfLoggedParameters = remaining > numberOfParameters ? numberOfParameters : remaining; - remaining -= numberOfLoggedParameters; - } - Iterator valueNames = null; - if (statement.usesNamedValues()) { - valueNames = statement.getValueNames().iterator(); - } - for (int i = 0; i < numberOfLoggedParameters; i++) { - if (buffer.length() == 0) buffer.append(" ["); - else buffer.append(", "); - if (valueNames != null && valueNames.hasNext()) { - String valueName = valueNames.next(); - buffer.append( - String.format( - "%s:%s", valueName, parameterValueAsString(statement.getObject(valueName)))); - } else { - buffer.append(parameterValueAsString(statement.getObject(i))); - } - } - if (numberOfLoggedParameters < numberOfParameters) { - buffer.append(FURTHER_PARAMS_OMITTED); - } - } - return remaining; - } - - protected String parameterValueAsString(Object value) { - String valueStr; - if (value == null) { - valueStr = "NULL"; - } else { - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - TypeCodec codec = codecRegistry.codecFor(value); - int maxParameterValueLength = this.maxParameterValueLength; - if (codec.cqlType.equals(DataType.blob()) && maxParameterValueLength != -1) { - // prevent large blobs from being converted to strings - ByteBuffer buf = (ByteBuffer) value; - int maxBufferLength = Math.max(2, (maxParameterValueLength - 2) / 2); - boolean bufferTooLarge = buf.remaining() > maxBufferLength; - if (bufferTooLarge) { - value = (ByteBuffer) buf.duplicate().limit(maxBufferLength); - } - valueStr = codec.format(value); - if (bufferTooLarge) { - valueStr = valueStr + TRUNCATED_OUTPUT; - } - } else { - valueStr = codec.format(value); - if (maxParameterValueLength != -1 && valueStr.length() > maxParameterValueLength) { - valueStr = valueStr.substring(0, maxParameterValueLength) + TRUNCATED_OUTPUT; - } - } - } - return valueStr; - } - - protected int appendParameters(BuiltStatement statement, StringBuilder buffer, int remaining) { - if (remaining == 0) { - return 0; - } - ByteBuffer[] values = - statement.getValues(protocolVersion(), cluster.getConfiguration().getCodecRegistry()); - int numberOfParameters = values == null ? 0 : values.length; - if (numberOfParameters > 0) { - int numberOfLoggedParameters; - if (remaining == -1) { - numberOfLoggedParameters = numberOfParameters; - } else { - numberOfLoggedParameters = remaining > numberOfParameters ? numberOfParameters : remaining; - remaining -= numberOfLoggedParameters; - } - - for (int i = 0; i < numberOfLoggedParameters; i++) { - if (buffer.length() == 0) { - buffer.append(" ["); - } else { - buffer.append(", "); - } - - buffer.append(parameterValueAsString(statement.getObject(i))); - } - if (numberOfLoggedParameters < numberOfParameters) { - buffer.append(FURTHER_PARAMS_OMITTED); - } - } - return remaining; - } - - private ProtocolVersion protocolVersion() { - // Since the QueryLogger can be registered before the Cluster was initialized, we can't retrieve - // it at construction time. Cache it field at first use (a volatile field is good enough since - // we - // don't need mutual exclusion). - if (protocolVersion == null) { - protocolVersion = cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - // At least one connection was established when QueryLogger is invoked - assert protocolVersion != null : "protocol version should be defined"; - } - return protocolVersion; - } - - protected int append(Statement statement, StringBuilder buffer, int remaining) { - if (statement instanceof RegularStatement) { - RegularStatement rs = (RegularStatement) statement; - String query = rs.getQueryString(); - remaining = append(query.trim(), buffer, remaining); - } else if (statement instanceof BoundStatement) { - remaining = - append( - ((BoundStatement) statement).preparedStatement().getQueryString().trim(), - buffer, - remaining); - } else if (statement instanceof BatchStatement) { - BatchStatement batchStatement = (BatchStatement) statement; - remaining = append("BEGIN", buffer, remaining); - switch (batchStatement.batchType) { - case UNLOGGED: - append(" UNLOGGED", buffer, remaining); - break; - case COUNTER: - append(" COUNTER", buffer, remaining); - break; - } - remaining = append(" BATCH", buffer, remaining); - for (Statement stmt : batchStatement.getStatements()) { - remaining = append(" ", buffer, remaining); - remaining = append(stmt, buffer, remaining); - } - remaining = append(" APPLY BATCH", buffer, remaining); - } else { - // Unknown types of statement - // Call toString() as a last resort - remaining = append(statement.toString(), buffer, remaining); - } - if (buffer.charAt(buffer.length() - 1) != ';') { - remaining = append(";", buffer, remaining); - } - return remaining; - } - - protected int append(CharSequence str, StringBuilder buffer, int remaining) { - if (remaining == -2) { - // capacity exceeded - } else if (remaining == -1) { - // unlimited capacity - buffer.append(str); - } else if (str.length() > remaining) { - buffer.append(str, 0, remaining).append(TRUNCATED_OUTPUT); - remaining = -2; - } else { - buffer.append(str); - remaining -= str.length(); - } - return remaining; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/QueryOptions.java b/driver-core/src/main/java/com/datastax/driver/core/QueryOptions.java deleted file mode 100644 index 556ee0a8a01..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/QueryOptions.java +++ /dev/null @@ -1,524 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.UnsupportedFeatureException; -import com.datastax.driver.core.utils.MoreFutures; -import com.datastax.driver.core.utils.MoreObjects; - -/** Options related to defaults for individual queries. */ -public class QueryOptions { - - /** The default consistency level for queries: {@link ConsistencyLevel#LOCAL_ONE}. */ - public static final ConsistencyLevel DEFAULT_CONSISTENCY_LEVEL = ConsistencyLevel.LOCAL_ONE; - - /** - * The default serial consistency level for conditional updates: {@link ConsistencyLevel#SERIAL}. - */ - public static final ConsistencyLevel DEFAULT_SERIAL_CONSISTENCY_LEVEL = ConsistencyLevel.SERIAL; - - /** The default fetch size for SELECT queries: 5000. */ - public static final int DEFAULT_FETCH_SIZE = 5000; - - /** The default value for {@link #getDefaultIdempotence()}: {@code false}. */ - public static final boolean DEFAULT_IDEMPOTENCE = false; - - public static final int DEFAULT_MAX_PENDING_REFRESH_NODE_LIST_REQUESTS = 20; - - public static final int DEFAULT_MAX_PENDING_REFRESH_NODE_REQUESTS = 20; - - public static final int DEFAULT_MAX_PENDING_REFRESH_SCHEMA_REQUESTS = 20; - - public static final int DEFAULT_REFRESH_NODE_LIST_INTERVAL_MILLIS = 1000; - - public static final int DEFAULT_REFRESH_NODE_INTERVAL_MILLIS = 1000; - - public static final int DEFAULT_REFRESH_SCHEMA_INTERVAL_MILLIS = 1000; - - private volatile ConsistencyLevel consistency = DEFAULT_CONSISTENCY_LEVEL; - private volatile ConsistencyLevel serialConsistency = DEFAULT_SERIAL_CONSISTENCY_LEVEL; - private volatile int fetchSize = DEFAULT_FETCH_SIZE; - private volatile boolean defaultIdempotence = DEFAULT_IDEMPOTENCE; - - private volatile boolean consistencySet = false; - private volatile boolean metadataEnabled = true; - - private volatile int maxPendingRefreshNodeListRequests = - DEFAULT_MAX_PENDING_REFRESH_NODE_LIST_REQUESTS; - private volatile int maxPendingRefreshNodeRequests = DEFAULT_MAX_PENDING_REFRESH_NODE_REQUESTS; - private volatile int maxPendingRefreshSchemaRequests = - DEFAULT_MAX_PENDING_REFRESH_SCHEMA_REQUESTS; - - private volatile int refreshNodeListIntervalMillis = DEFAULT_REFRESH_NODE_LIST_INTERVAL_MILLIS; - private volatile int refreshNodeIntervalMillis = DEFAULT_REFRESH_NODE_INTERVAL_MILLIS; - private volatile int refreshSchemaIntervalMillis = DEFAULT_REFRESH_SCHEMA_INTERVAL_MILLIS; - - private volatile boolean reprepareOnUp = true; - private volatile Cluster.Manager manager; - private volatile boolean prepareOnAllHosts = true; - - /** - * Creates a new {@link QueryOptions} instance using the {@link #DEFAULT_CONSISTENCY_LEVEL}, - * {@link #DEFAULT_SERIAL_CONSISTENCY_LEVEL} and {@link #DEFAULT_FETCH_SIZE}. - */ - public QueryOptions() {} - - void register(Cluster.Manager manager) { - this.manager = manager; - } - - /** - * Sets the default consistency level to use for queries. - * - *

The consistency level set through this method will be use for queries that don't explicitly - * have a consistency level, i.e. when {@link Statement#getConsistencyLevel} returns {@code null}. - * - * @param consistencyLevel the new consistency level to set as default. - * @return this {@code QueryOptions} instance. - */ - public QueryOptions setConsistencyLevel(ConsistencyLevel consistencyLevel) { - this.consistencySet = true; - this.consistency = consistencyLevel; - return this; - } - - /** - * The default consistency level used by queries. - * - * @return the default consistency level used by queries. - */ - public ConsistencyLevel getConsistencyLevel() { - return consistency; - } - - /** - * Sets the default serial consistency level to use for queries. - * - *

The serial consistency level set through this method will be use for queries that don't - * explicitly have a serial consistency level, i.e. when {@link - * Statement#getSerialConsistencyLevel} returns {@code null}. - * - * @param serialConsistencyLevel the new serial consistency level to set as default. - * @return this {@code QueryOptions} instance. - */ - public QueryOptions setSerialConsistencyLevel(ConsistencyLevel serialConsistencyLevel) { - this.serialConsistency = serialConsistencyLevel; - return this; - } - - /** - * The default serial consistency level used by queries. - * - * @return the default serial consistency level used by queries. - */ - public ConsistencyLevel getSerialConsistencyLevel() { - return serialConsistency; - } - - /** - * Sets the default fetch size to use for SELECT queries. - * - *

The fetch size set through this method will be use for queries that don't explicitly have a - * fetch size, i.e. when {@link Statement#getFetchSize} is less or equal to 0. - * - * @param fetchSize the new fetch size to set as default. It must be strictly positive but you can - * use {@code Integer.MAX_VALUE} to disable paging. - * @return this {@code QueryOptions} instance. - * @throws IllegalArgumentException if {@code fetchSize <e; 0}. - * @throws UnsupportedFeatureException if version 1 of the native protocol is in use and {@code - * fetchSize != Integer.MAX_VALUE} as paging is not supported by version 1 of the protocol. - * See {@link Cluster.Builder#withProtocolVersion} for more details on protocol versions. - */ - public QueryOptions setFetchSize(int fetchSize) { - if (fetchSize <= 0) - throw new IllegalArgumentException("Invalid fetchSize, should be > 0, got " + fetchSize); - - ProtocolVersion version = manager == null ? null : manager.protocolVersion(); - if (fetchSize != Integer.MAX_VALUE && version == ProtocolVersion.V1) - throw new UnsupportedFeatureException(version, "Paging is not supported"); - - this.fetchSize = fetchSize; - return this; - } - - /** - * The default fetch size used by queries. - * - * @return the default fetch size used by queries. - */ - public int getFetchSize() { - return fetchSize; - } - - /** - * Sets the default idempotence for queries. - * - *

This will be used for statements for which {@link - * com.datastax.driver.core.Statement#isIdempotent()} returns {@code null}. - * - * @param defaultIdempotence the new value to set as default idempotence. - * @return this {@code QueryOptions} instance. - */ - public QueryOptions setDefaultIdempotence(boolean defaultIdempotence) { - this.defaultIdempotence = defaultIdempotence; - return this; - } - - /** - * The default idempotence for queries. - * - *

It defaults to {@link #DEFAULT_IDEMPOTENCE}. - * - * @return the default idempotence for queries. - */ - public boolean getDefaultIdempotence() { - return defaultIdempotence; - } - - /** - * Set whether the driver should prepare statements on all hosts in the cluster. - * - *

A statement is normally prepared in two steps: - * - *

    - *
  1. prepare the query on a single host in the cluster; - *
  2. if that succeeds, prepare on all other hosts. - *
- * - * This option controls whether step 2 is executed. It is enabled by default. - * - *

The reason why you might want to disable it is to optimize network usage if you have a large - * number of clients preparing the same set of statements at startup. If your load balancing - * policy distributes queries randomly, each client will pick a different host to prepare its - * statements, and on the whole each host has a good chance of having been hit by at least one - * client for each statement. - * - *

On the other hand, if that assumption turns out to be wrong and one host hasn't prepared a - * given statement, it needs to be re-prepared on the fly the first time it gets executed; this - * causes a performance penalty (one extra roundtrip to resend the query to prepare, and another - * to retry the execution). - * - * @param prepareOnAllHosts the new value to set to indicate whether to prepare statements once or - * on all nodes. - * @return this {@code QueryOptions} instance. - */ - public QueryOptions setPrepareOnAllHosts(boolean prepareOnAllHosts) { - this.prepareOnAllHosts = prepareOnAllHosts; - return this; - } - - /** - * Returns whether the driver should prepare statements on all hosts in the cluster. - * - * @return the value. - * @see #setPrepareOnAllHosts(boolean) - */ - public boolean isPrepareOnAllHosts() { - return this.prepareOnAllHosts; - } - - /** - * Set whether the driver should re-prepare all cached prepared statements on a host when it marks - * it back up. - * - *

This option is enabled by default. - * - *

The reason why you might want to disable it is to optimize reconnection time when you - * believe hosts often get marked down because of temporary network issues, rather than the host - * really crashing. In that case, the host still has prepared statements in its cache when the - * driver reconnects, so re-preparing is redundant. - * - *

On the other hand, if that assumption turns out to be wrong and the host had really - * restarted, its prepared statement cache is empty, and statements need to be re-prepared on the - * fly the first time they get executed; this causes a performance penalty (one extra roundtrip to - * resend the query to prepare, and another to retry the execution). - * - * @param reprepareOnUp whether the driver should re-prepare when marking a node up. - * @return this {@code QueryOptions} instance. - */ - public QueryOptions setReprepareOnUp(boolean reprepareOnUp) { - this.reprepareOnUp = reprepareOnUp; - return this; - } - - /** - * Whether the driver should re-prepare all cached prepared statements on a host when its marks - * that host back up. - * - * @return the value. - * @see #setReprepareOnUp(boolean) - */ - public boolean isReprepareOnUp() { - return this.reprepareOnUp; - } - - /** - * Toggle client-side token and schema metadata. - * - *

This feature is enabled by default. Some applications might wish to disable it in order to - * eliminate the overhead of querying the metadata and building its client-side representation. - * However, take note that doing so will have important consequences: - * - *

    - *
  • most schema- or token-related methods in {@link Metadata} will return stale or null/empty - * results (see the javadoc of each method for details); - *
  • {@link Metadata#newToken(String)} and {@link Metadata#newTokenRange(Token, Token)} will - * throw an exception if metadata was disabled before startup; - *
  • token-aware routing will not work properly: if metadata was never initialized, {@link - * com.datastax.driver.core.policies.TokenAwarePolicy} will always delegate to its child - * policy. Otherwise, it might not pick the best coordinator (i.e. chose a host that is not - * a replica for the statement's routing key). In addition, statements prepared while the - * metadata was disabled might also be sent to a non-optimal coordinator, even if metadata - * was re-enabled later. - *
- * - * @param enabled whether metadata is enabled. - * @return this {@code QueryOptions} instance. - */ - public QueryOptions setMetadataEnabled(boolean enabled) { - boolean wasEnabled = this.metadataEnabled; - this.metadataEnabled = enabled; - if (!wasEnabled && enabled && manager != null) { - // This is roughly the same as what we do in ControlConnection.tryConnect(): - // 1. call submitNodeListRefresh() first to - // be able to compute the token map for the first time, - // which will be incomplete due to the lack of keyspace metadata - GuavaCompatibility.INSTANCE.addCallback( - manager.submitNodeListRefresh(), - new MoreFutures.SuccessCallback() { - @Override - public void onSuccess(Void result) { - // 2. then call submitSchemaRefresh() to - // refresh schema metadata and re-compute the token map - // this time with information about keyspaces - manager.submitSchemaRefresh(null, null, null, null); - } - }); - } - return this; - } - - /** - * Whether client-side token and schema metadata is enabled. - * - * @return the value. - * @see #setMetadataEnabled(boolean) - */ - public boolean isMetadataEnabled() { - return metadataEnabled; - } - - /** - * Sets the default window size in milliseconds used to debounce node list refresh requests. - * - *

When the control connection receives a new schema refresh request, it puts it on hold and - * starts a timer, cancelling any previous running timer; when a timer expires, then the pending - * requests are coalesced and executed as a single request. - * - * @param refreshSchemaIntervalMillis The default window size in milliseconds used to debounce - * schema refresh requests. - */ - public QueryOptions setRefreshSchemaIntervalMillis(int refreshSchemaIntervalMillis) { - this.refreshSchemaIntervalMillis = refreshSchemaIntervalMillis; - return this; - } - - /** - * The default window size in milliseconds used to debounce schema refresh requests. - * - * @return The default window size in milliseconds used to debounce schema refresh requests. - */ - public int getRefreshSchemaIntervalMillis() { - return refreshSchemaIntervalMillis; - } - - /** - * Sets the maximum number of schema refresh requests that the control connection can accumulate - * before executing them. - * - *

When the control connection receives a new schema refresh request, it puts it on hold and - * starts a timer, cancelling any previous running timer; if the control connection receives too - * many events, is parameter allows to trigger execution of pending requests, event if the last - * timer is still running. - * - * @param maxPendingRefreshSchemaRequests The maximum number of schema refresh requests that the - * control connection can accumulate before executing them. - */ - public QueryOptions setMaxPendingRefreshSchemaRequests(int maxPendingRefreshSchemaRequests) { - this.maxPendingRefreshSchemaRequests = maxPendingRefreshSchemaRequests; - return this; - } - - /** - * The maximum number of schema refresh requests that the control connection can accumulate before - * executing them. - * - * @return The maximum number of schema refresh requests that the control connection can - * accumulate before executing them. - */ - public int getMaxPendingRefreshSchemaRequests() { - return maxPendingRefreshSchemaRequests; - } - - /** - * Sets the default window size in milliseconds used to debounce node list refresh requests. - * - *

When the control connection receives a new node list refresh request, it puts it on hold and - * starts a timer, cancelling any previous running timer; when a timer expires, then the pending - * requests are coalesced and executed as a single request. - * - * @param refreshNodeListIntervalMillis The default window size in milliseconds used to debounce - * node list refresh requests. - */ - public QueryOptions setRefreshNodeListIntervalMillis(int refreshNodeListIntervalMillis) { - this.refreshNodeListIntervalMillis = refreshNodeListIntervalMillis; - return this; - } - - /** - * The default window size in milliseconds used to debounce node list refresh requests. - * - * @return The default window size in milliseconds used to debounce node list refresh requests. - */ - public int getRefreshNodeListIntervalMillis() { - return refreshNodeListIntervalMillis; - } - - /** - * Sets the maximum number of node list refresh requests that the control connection can - * accumulate before executing them. - * - *

When the control connection receives a new node list refresh request, it puts it on hold and - * starts a timer, cancelling any previous running timer; if the control connection receives too - * many events, is parameter allows to trigger execution of pending requests, event if the last - * timer is still running. - * - * @param maxPendingRefreshNodeListRequests The maximum number of node list refresh requests that - * the control connection can accumulate before executing them. - */ - public QueryOptions setMaxPendingRefreshNodeListRequests(int maxPendingRefreshNodeListRequests) { - this.maxPendingRefreshNodeListRequests = maxPendingRefreshNodeListRequests; - return this; - } - - /** - * Sets the maximum number of node list refresh requests that the control connection can - * accumulate before executing them. - * - * @return The maximum number of node list refresh requests that the control connection can - * accumulate before executing them. - */ - public int getMaxPendingRefreshNodeListRequests() { - return maxPendingRefreshNodeListRequests; - } - - /** - * Sets the default window size in milliseconds used to debounce node refresh requests. - * - *

When the control connection receives a new node refresh request, it puts it on hold and - * starts a timer, cancelling any previous running timer; when a timer expires, then the pending - * requests are coalesced and executed as a single request. - * - * @param refreshNodeIntervalMillis The default window size in milliseconds used to debounce node - * refresh requests. - */ - public QueryOptions setRefreshNodeIntervalMillis(int refreshNodeIntervalMillis) { - this.refreshNodeIntervalMillis = refreshNodeIntervalMillis; - return this; - } - - /** - * The default window size in milliseconds used to debounce node refresh requests. - * - * @return The default window size in milliseconds used to debounce node refresh requests. - */ - public int getRefreshNodeIntervalMillis() { - return refreshNodeIntervalMillis; - } - - /** - * Sets the maximum number of node refresh requests that the control connection can accumulate - * before executing them. - * - *

When the control connection receives a new node refresh request, it puts it on hold and - * starts a timer, cancelling any previous running timer; if the control connection receives too - * many events, is parameter allows to trigger execution of pending requests, event if the last - * timer is still running. - * - * @param maxPendingRefreshNodeRequests The maximum number of node refresh requests that the - * control connection can accumulate before executing them. - */ - public QueryOptions setMaxPendingRefreshNodeRequests(int maxPendingRefreshNodeRequests) { - this.maxPendingRefreshNodeRequests = maxPendingRefreshNodeRequests; - return this; - } - - /** - * The maximum number of node refresh requests that the control connection can accumulate before - * executing them. - * - * @return The maximum number of node refresh requests that the control connection can accumulate - * before executing them. - */ - public int getMaxPendingRefreshNodeRequests() { - return maxPendingRefreshNodeRequests; - } - - @Override - public boolean equals(Object that) { - if (that == null || !(that instanceof QueryOptions)) { - return false; - } - - QueryOptions other = (QueryOptions) that; - - return (this.consistency.equals(other.consistency) - && this.serialConsistency.equals(other.serialConsistency) - && this.fetchSize == other.fetchSize - && this.defaultIdempotence == other.defaultIdempotence - && this.metadataEnabled == other.metadataEnabled - && this.maxPendingRefreshNodeListRequests == other.maxPendingRefreshNodeListRequests - && this.maxPendingRefreshNodeRequests == other.maxPendingRefreshNodeRequests - && this.maxPendingRefreshSchemaRequests == other.maxPendingRefreshSchemaRequests - && this.refreshNodeListIntervalMillis == other.refreshNodeListIntervalMillis - && this.refreshNodeIntervalMillis == other.refreshNodeIntervalMillis - && this.refreshSchemaIntervalMillis == other.refreshSchemaIntervalMillis - && this.reprepareOnUp == other.reprepareOnUp - && this.prepareOnAllHosts == other.prepareOnAllHosts); - } - - @Override - public int hashCode() { - return MoreObjects.hashCode( - consistency, - serialConsistency, - fetchSize, - defaultIdempotence, - metadataEnabled, - maxPendingRefreshNodeListRequests, - maxPendingRefreshNodeRequests, - maxPendingRefreshSchemaRequests, - refreshNodeListIntervalMillis, - refreshNodeIntervalMillis, - refreshSchemaIntervalMillis, - reprepareOnUp, - prepareOnAllHosts); - } - - public boolean isConsistencySet() { - return consistencySet; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/QueryTrace.java b/driver-core/src/main/java/com/datastax/driver/core/QueryTrace.java deleted file mode 100644 index 6ab199fe4b0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/QueryTrace.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.TraceRetrievalException; -import com.google.common.util.concurrent.Uninterruptibles; -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -/** - * The Cassandra trace for a query. - * - *

A trace is generated by Cassandra when query tracing is enabled for the query. The trace - * itself is stored in Cassandra in the {@code sessions} and {@code events} table in the {@code - * system_traces} keyspace and can be retrieve manually using the trace identifier (the one returned - * by {@link #getTraceId}). - * - *

This class provides facilities to fetch the traces from Cassandra. Please note that the - * writing of the trace is done asynchronously in Cassandra. So accessing the trace too soon after - * the query may result in the trace being incomplete. - */ -public class QueryTrace { - private static final String SELECT_SESSIONS_FORMAT = - "SELECT * FROM system_traces.sessions WHERE session_id = %s"; - private static final String SELECT_EVENTS_FORMAT = - "SELECT * FROM system_traces.events WHERE session_id = %s"; - - private static final int MAX_TRIES = 5; - private static final long BASE_SLEEP_BETWEEN_TRIES_IN_MS = 3; - - private final UUID traceId; - - private volatile String requestType; - // We use the duration to figure out if the trace is complete, because - // that's the last event that is written (and it is written asynchronously - // so it's possible that a fetch gets all the trace except the duration). - private volatile int duration = Integer.MIN_VALUE; - private volatile InetAddress coordinator; - private volatile Map parameters; - private volatile long startedAt; - private volatile List events; - - private final SessionManager session; - private final Lock fetchLock = new ReentrantLock(); - - QueryTrace(UUID traceId, SessionManager session) { - this.traceId = traceId; - this.session = session; - } - - /** - * Returns the identifier of this trace. - * - *

Note that contrary to the other methods in this class, this does not entail fetching query - * trace details from Cassandra. - * - * @return the identifier of this trace. - */ - public UUID getTraceId() { - return traceId; - } - - /** - * Returns the type of request. - * - * @return the type of request or {@code null} if the request type is not yet available. - * @throws TraceRetrievalException if the trace details cannot be retrieve from Cassandra - * successfully. - */ - public String getRequestType() { - maybeFetchTrace(); - return requestType; - } - - /** - * Returns the server-side duration of the query in microseconds. - * - * @return the (server side) duration of the query in microseconds. This method will return {@code - * Integer.MIN_VALUE} if the duration is not yet available. - * @throws TraceRetrievalException if the trace details cannot be retrieve from Cassandra - * successfully. - */ - public int getDurationMicros() { - maybeFetchTrace(); - return duration; - } - - /** - * Returns the coordinator host of the query. - * - * @return the coordinator host of the query or {@code null} if the coordinator is not yet - * available. - * @throws TraceRetrievalException if the trace details cannot be retrieve from Cassandra - * successfully. - */ - public InetAddress getCoordinator() { - maybeFetchTrace(); - return coordinator; - } - - /** - * Returns the parameters attached to this trace. - * - * @return the parameters attached to this trace. or {@code null} if the coordinator is not yet - * available. - * @throws TraceRetrievalException if the trace details cannot be retrieve from Cassandra - * successfully. - */ - public Map getParameters() { - maybeFetchTrace(); - return parameters; - } - - /** - * Returns the server-side timestamp of the start of this query. - * - * @return the server side timestamp of the start of this query or 0 if the start timestamp is not - * available. - * @throws TraceRetrievalException if the trace details cannot be retrieve from Cassandra - * successfully. - */ - public long getStartedAt() { - maybeFetchTrace(); - return startedAt; - } - - /** - * Returns the events contained in this trace. - * - *

Query tracing is asynchronous in Cassandra. Hence, it is possible for the list returned to - * be missing some events for some of the replica involved in the query if the query trace is - * requested just after the return of the query it is a trace of (the only guarantee being that - * the list will contain the events pertaining to the coordinator of the query). - * - * @return the events contained in this trace. - * @throws TraceRetrievalException if the trace details cannot be retrieve from Cassandra - * successfully. - */ - public List getEvents() { - maybeFetchTrace(); - return events; - } - - @Override - public String toString() { - maybeFetchTrace(); - return String.format("%s [%s] - %dµs", requestType, traceId, duration); - } - - private void maybeFetchTrace() { - if (duration != Integer.MIN_VALUE) return; - - fetchLock.lock(); - try { - doFetchTrace(); - } finally { - fetchLock.unlock(); - } - } - - private void doFetchTrace() { - int tries = 0; - try { - // We cannot guarantee the trace is complete. But we can't at least wait until we have all the - // information - // the coordinator log in the trace. Since the duration is the last thing the coordinator log, - // that's - // what we check to know if the trace is "complete" (again, it may not contain the log of - // replicas). - while (duration == Integer.MIN_VALUE && tries <= MAX_TRIES) { - ++tries; - - ResultSetFuture sessionsFuture = - session.executeQuery( - new Requests.Query(String.format(SELECT_SESSIONS_FORMAT, traceId)), - Statement.DEFAULT); - ResultSetFuture eventsFuture = - session.executeQuery( - new Requests.Query(String.format(SELECT_EVENTS_FORMAT, traceId)), - Statement.DEFAULT); - - Row sessRow = sessionsFuture.get().one(); - if (sessRow != null && !sessRow.isNull("duration")) { - - requestType = sessRow.getString("request"); - coordinator = sessRow.getInet("coordinator"); - if (!sessRow.isNull("parameters")) - parameters = - Collections.unmodifiableMap( - sessRow.getMap("parameters", String.class, String.class)); - startedAt = sessRow.getTimestamp("started_at").getTime(); - - events = new ArrayList(); - for (Row evRow : eventsFuture.get()) { - events.add( - new Event( - evRow.getString("activity"), - evRow.getUUID("event_id").timestamp(), - evRow.getInet("source"), - evRow.getInt("source_elapsed"), - evRow.getString("thread"))); - } - events = Collections.unmodifiableList(events); - - // Set the duration last as it's our test to know if the trace is complete - duration = sessRow.getInt("duration"); - } else { - // The trace is not ready. Give it a few milliseconds before trying again. - // Notes: granted, sleeping uninterruptibly is bad, but having all method propagate - // InterruptedException bothers me. - Uninterruptibles.sleepUninterruptibly( - tries * BASE_SLEEP_BETWEEN_TRIES_IN_MS, TimeUnit.MILLISECONDS); - } - } - } catch (Exception e) { - throw new TraceRetrievalException("Unexpected exception while fetching query trace", e); - } - - if (tries > MAX_TRIES) - throw new TraceRetrievalException( - String.format( - "Unable to retrieve complete query trace for id %s after %d tries", - traceId, MAX_TRIES)); - } - - /** - * A trace event. - * - *

A query trace is composed of a list of trace events. - */ - public static class Event { - private final String name; - private final long timestamp; - private final InetAddress source; - private final int sourceElapsed; - private final String threadName; - - private Event( - String name, long timestamp, InetAddress source, int sourceElapsed, String threadName) { - this.name = name; - // Convert the UUID timestamp to an epoch timestamp; I stole this seemingly random value from - // cqlsh, hopefully it's correct. - this.timestamp = (timestamp - 0x01b21dd213814000L) / 10000; - this.source = source; - this.sourceElapsed = sourceElapsed; - this.threadName = threadName; - } - - /** - * The event description, that is which activity this event correspond to. - * - * @return the event description. - */ - public String getDescription() { - return name; - } - - /** - * Returns the server side timestamp of the event. - * - * @return the server side timestamp of the event. - */ - public long getTimestamp() { - return timestamp; - } - - /** - * Returns the address of the host having generated this event. - * - * @return the address of the host having generated this event. - */ - public InetAddress getSource() { - return source; - } - - /** - * Returns the number of microseconds elapsed on the source when this event occurred since when - * the source started handling the query. - * - * @return the elapsed time on the source host when that event happened in microseconds. - */ - public int getSourceElapsedMicros() { - return sourceElapsed; - } - - /** - * Returns the name of the thread on which this event occurred. - * - * @return the name of the thread on which this event occurred. - */ - public String getThreadName() { - return threadName; - } - - @Override - public String toString() { - return String.format("%s on %s[%s] at %s", name, source, threadName, new Date(timestamp)); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/RegularStatement.java b/driver-core/src/main/java/com/datastax/driver/core/RegularStatement.java deleted file mode 100644 index 3a9e8fe90d7..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/RegularStatement.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.Frame.Header; -import com.datastax.driver.core.Requests.QueryFlag; -import com.datastax.driver.core.exceptions.InvalidTypeException; -import com.datastax.driver.core.exceptions.UnsupportedProtocolVersionException; -import com.datastax.driver.core.querybuilder.BuiltStatement; -import com.datastax.driver.core.schemabuilder.SchemaStatement; -import java.nio.ByteBuffer; -import java.util.Map; - -/** - * A regular (non-prepared and non batched) CQL statement. - * - *

This class represents a query string along with query options (and optionally binary values, - * see {@code getValues}). It can be extended but {@link SimpleStatement} is provided as a simple - * implementation to build a {@code RegularStatement} directly from its query string. - */ -public abstract class RegularStatement extends Statement { - - /** Creates a new RegularStatement. */ - protected RegularStatement() {} - - /** - * Returns the query string for this statement. - * - *

It is important to note that the query string is merely a CQL representation of this - * statement, but it does not convey all the information stored in {@link Statement} - * objects. - * - *

For example, {@link Statement} objects carry numerous protocol-level settings, such as the - * {@link Statement#getConsistencyLevel() consistency level} to use, or the {@link - * Statement#isIdempotent() idempotence flag}, among others. None of these settings will be - * included in the resulting query string. - * - *

Similarly, if values have been set on this statement because it has bind markers, these - * values will not appear in the resulting query string. - * - *

Note: the consistency level was conveyed at CQL level in older versions of the CQL grammar, - * but since CASSANDRA-4734 it - * is now a protocol-level setting and consequently does not appear in the query string. - * - * @param codecRegistry the codec registry that will be used if the actual implementation needs to - * serialize Java objects in the process of generating the query. Note that it might be - * possible to use the no-arg {@link #getQueryString()} depending on the type of statement - * this is called on. - * @return a valid CQL query string. - * @see #getQueryString() - */ - public abstract String getQueryString(CodecRegistry codecRegistry); - - /** - * Returns the query string for this statement. - * - *

This method calls {@link #getQueryString(CodecRegistry)} with {@link - * CodecRegistry#DEFAULT_INSTANCE}. Whether you should use this or the other variant depends on - * the type of statement this is called on: - * - *

    - *
  • for a {@link SimpleStatement} or {@link SchemaStatement}, the codec registry isn't - * actually needed, so it's always safe to use this method; - *
  • for a {@link BuiltStatement} you can use this method if you use no custom codecs, or if - * your custom codecs are registered with the default registry. Otherwise, use the other - * method and provide the registry that contains your codecs (see {@link BuiltStatement} for - * more explanations on why this is so); - *
  • for a {@link BatchStatement}, use the first rule if it contains no built statements, or - * the second rule otherwise. - *
- * - * @return a valid CQL query string. - */ - public String getQueryString() { - return getQueryString(CodecRegistry.DEFAULT_INSTANCE); - } - - /** - * The positional values to use for this statement. - * - *

A statement can use either positional or named values, but not both. So if this method - * returns a non-null result, {@link #getNamedValues(ProtocolVersion, CodecRegistry)} will return - * {@code null}. - * - *

Values for a RegularStatement (i.e. if either method does not return {@code null}) are not - * supported with the native protocol version 1: you will get an {@link - * UnsupportedProtocolVersionException} when submitting one if version 1 of the protocol is in use - * (i.e. if you've forced version 1 through {@link Cluster.Builder#withProtocolVersion} or you use - * Cassandra 1.2). - * - * @param protocolVersion the protocol version that will be used to serialize the values. - * @param codecRegistry the codec registry that will be used to serialize the values. - * @throws InvalidTypeException if one of the values is not of a type that can be serialized to a - * CQL3 type - * @see SimpleStatement#SimpleStatement(String, Object...) - */ - public abstract ByteBuffer[] getValues( - ProtocolVersion protocolVersion, CodecRegistry codecRegistry); - - /** - * The named values to use for this statement. - * - *

A statement can use either positional or named values, but not both. So if this method - * returns a non-null result, {@link #getValues(ProtocolVersion, CodecRegistry)} will return - * {@code null}. - * - *

Values for a RegularStatement (i.e. if either method does not return {@code null}) are not - * supported with the native protocol version 1: you will get an {@link - * UnsupportedProtocolVersionException} when submitting one if version 1 of the protocol is in use - * (i.e. if you've forced version 1 through {@link Cluster.Builder#withProtocolVersion} or you use - * Cassandra 1.2). - * - * @param protocolVersion the protocol version that will be used to serialize the values. - * @param codecRegistry the codec registry that will be used to serialize the values. - * @return the named values. - * @throws InvalidTypeException if one of the values is not of a type that can be serialized to a - * CQL3 type - * @see SimpleStatement#SimpleStatement(String, Map) - */ - public abstract Map getNamedValues( - ProtocolVersion protocolVersion, CodecRegistry codecRegistry); - - /** - * Whether or not this statement has values, that is if {@code getValues} will return {@code null} - * or not. - * - * @param codecRegistry the codec registry that will be used if the actual implementation needs to - * serialize Java objects in the process of determining if the query has values. Note that it - * might be possible to use the no-arg {@link #hasValues()} depending on the type of statement - * this is called on. - * @return {@code false} if both {@link #getValues(ProtocolVersion, CodecRegistry)} and {@link - * #getNamedValues(ProtocolVersion, CodecRegistry)} return {@code null}, {@code true} - * otherwise. - * @see #hasValues() - */ - public abstract boolean hasValues(CodecRegistry codecRegistry); - - /** - * Whether this statement uses named values. - * - * @return {@code false} if {@link #getNamedValues(ProtocolVersion, CodecRegistry)} returns {@code - * null}, {@code true} otherwise. - */ - public abstract boolean usesNamedValues(); - - /** - * Whether or not this statement has values, that is if {@code getValues} will return {@code null} - * or not. - * - *

This method calls {@link #hasValues(CodecRegistry)} with {@link - * ProtocolVersion#NEWEST_SUPPORTED}. Whether you should use this or the other variant depends on - * the type of statement this is called on: - * - *

    - *
  • for a {@link SimpleStatement} or {@link SchemaStatement}, the codec registry isn't - * actually needed, so it's always safe to use this method; - *
  • for a {@link BuiltStatement} you can use this method if you use no custom codecs, or if - * your custom codecs are registered with the default registry. Otherwise, use the other - * method and provide the registry that contains your codecs (see {@link BuiltStatement} for - * more explanations on why this is so); - *
  • for a {@link BatchStatement}, use the first rule if it contains no built statements, or - * the second rule otherwise. - *
- * - * @return {@code false} if {@link #getValues} returns {@code null}, {@code true} otherwise. - */ - public boolean hasValues() { - return hasValues(CodecRegistry.DEFAULT_INSTANCE); - } - - @Override - public int requestSizeInBytes(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - int size = Header.lengthFor(protocolVersion); - try { - size += CBUtil.sizeOfLongString(getQueryString(codecRegistry)); - switch (protocolVersion) { - case V1: - size += CBUtil.sizeOfConsistencyLevel(getConsistencyLevel()); - break; - case V2: - case V3: - case V4: - case V5: - case V6: - size += CBUtil.sizeOfConsistencyLevel(getConsistencyLevel()); - size += QueryFlag.serializedSize(protocolVersion); - if (hasValues()) { - if (usesNamedValues()) { - size += CBUtil.sizeOfNamedValueList(getNamedValues(protocolVersion, codecRegistry)); - } else { - size += CBUtil.sizeOfValueList(getValues(protocolVersion, codecRegistry)); - } - } - // Fetch size, serial CL and default timestamp also depend on session-level defaults - // (QueryOptions). - // We always count them to avoid having to inject QueryOptions here, at worst we - // overestimate by a - // few bytes. - size += 4; // fetch size - if (getPagingState() != null) { - size += CBUtil.sizeOfValue(getPagingState()); - } - size += CBUtil.sizeOfConsistencyLevel(getSerialConsistencyLevel()); - if (ProtocolFeature.CLIENT_TIMESTAMPS.isSupportedBy(protocolVersion)) { - size += 8; // timestamp - } - if (ProtocolFeature.CUSTOM_PAYLOADS.isSupportedBy(protocolVersion) - && getOutgoingPayload() != null) { - size += CBUtil.sizeOfBytesMap(getOutgoingPayload()); - } - break; - default: - throw protocolVersion.unsupported(); - } - } catch (Exception e) { - size = -1; - } - return size; - } - - /** - * Returns this statement as a CQL query string. - * - *

It is important to note that the query string is merely a CQL representation of this - * statement, but it does not convey all the information stored in {@link Statement} - * objects. - * - *

See the javadocs of {@link #getQueryString()} for more information. - * - * @return this statement as a CQL query string. - * @see #getQueryString() - */ - @Override - public String toString() { - return getQueryString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/RemoteEndpointAwareJdkSSLOptions.java b/driver-core/src/main/java/com/datastax/driver/core/RemoteEndpointAwareJdkSSLOptions.java deleted file mode 100644 index 17db85a42cd..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/RemoteEndpointAwareJdkSSLOptions.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.ssl.SslHandler; -import java.net.InetSocketAddress; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; - -/** - * {@link RemoteEndpointAwareSSLOptions} implementation based on built-in JDK classes. - * - * @see JAVA-1364 - * @since 3.2.0 - */ -@SuppressWarnings("deprecation") -public class RemoteEndpointAwareJdkSSLOptions extends JdkSSLOptions - implements ExtendedRemoteEndpointAwareSslOptions { - - /** - * Creates a builder to create a new instance. - * - * @return the builder. - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Creates a new instance. - * - * @param context the SSL context. - * @param cipherSuites the cipher suites to use. - */ - protected RemoteEndpointAwareJdkSSLOptions(SSLContext context, String[] cipherSuites) { - super(context, cipherSuites); - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel) { - throw new AssertionError( - "This class implements RemoteEndpointAwareSSLOptions, this method should not be called"); - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel, EndPoint remoteEndpoint) { - SSLEngine engine = - newSSLEngine(channel, remoteEndpoint == null ? null : remoteEndpoint.resolve()); - return new SslHandler(engine); - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel, InetSocketAddress remoteEndpoint) { - throw new AssertionError( - "The driver should never call this method on an object that implements " - + this.getClass().getSimpleName()); - } - - /** - * Creates an SSL engine each time a connection is established. - * - *

You might want to override this if you need to fine-tune the engine's configuration (for - * example enabling hostname verification). - * - * @param channel the Netty channel for that connection. - * @param remoteEndpoint the remote endpoint we are connecting to. - * @return the engine. - * @since 3.2.0 - */ - protected SSLEngine newSSLEngine( - @SuppressWarnings("unused") SocketChannel channel, InetSocketAddress remoteEndpoint) { - SSLEngine engine; - if (remoteEndpoint == null) { - engine = context.createSSLEngine(); - } else { - engine = context.createSSLEngine(remoteEndpoint.getHostName(), remoteEndpoint.getPort()); - } - engine.setUseClientMode(true); - if (cipherSuites != null) engine.setEnabledCipherSuites(cipherSuites); - return engine; - } - - /** Helper class to build {@link RemoteEndpointAwareJdkSSLOptions} instances. */ - public static class Builder extends JdkSSLOptions.Builder { - - @Override - public RemoteEndpointAwareJdkSSLOptions.Builder withSSLContext(SSLContext context) { - super.withSSLContext(context); - return this; - } - - @Override - public RemoteEndpointAwareJdkSSLOptions.Builder withCipherSuites(String[] cipherSuites) { - super.withCipherSuites(cipherSuites); - return this; - } - - @Override - public RemoteEndpointAwareJdkSSLOptions build() { - return new RemoteEndpointAwareJdkSSLOptions(context, cipherSuites); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/RemoteEndpointAwareNettySSLOptions.java b/driver-core/src/main/java/com/datastax/driver/core/RemoteEndpointAwareNettySSLOptions.java deleted file mode 100644 index 5bf69ed0517..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/RemoteEndpointAwareNettySSLOptions.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslHandler; -import java.net.InetSocketAddress; - -/** - * {@link RemoteEndpointAwareSSLOptions} implementation based on Netty's SSL context. - * - *

Netty has the ability to use OpenSSL if available, instead of the JDK's built-in engine. This - * yields better performance. - * - * @see JAVA-1364 - * @since 3.2.0 - */ -@SuppressWarnings("deprecation") -public class RemoteEndpointAwareNettySSLOptions extends NettySSLOptions - implements ExtendedRemoteEndpointAwareSslOptions { - - /** - * Create a new instance from a given context. - * - * @param context the Netty context. {@code SslContextBuilder.forClient()} provides a fluent API - * to build it. - */ - public RemoteEndpointAwareNettySSLOptions(SslContext context) { - super(context); - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel) { - throw new AssertionError( - "This class implements RemoteEndpointAwareSSLOptions, this method should not be called"); - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel, EndPoint remoteEndpoint) { - InetSocketAddress address = remoteEndpoint.resolve(); - return context.newHandler(channel.alloc(), address.getHostName(), address.getPort()); - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel, InetSocketAddress remoteEndpoint) { - throw new AssertionError( - "The driver should never call this method on an object that implements " - + this.getClass().getSimpleName()); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/RemoteEndpointAwareSSLOptions.java b/driver-core/src/main/java/com/datastax/driver/core/RemoteEndpointAwareSSLOptions.java deleted file mode 100644 index 32d709076a1..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/RemoteEndpointAwareSSLOptions.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.ssl.SslHandler; -import java.net.InetSocketAddress; - -/** - * Child interface to {@link SSLOptions} with the possibility to pass remote endpoint data when - * instantiating {@link SslHandler}s. - * - *

This is needed when e.g. hostname verification is required. See JAVA-1364 for details. - * - *

The reason this is a child interface is to keep {@link SSLOptions} backwards-compatible. This - * interface may be be merged into {@link SSLOptions} in a later major release. - * - * @see JAVA-1364 - * @since 3.2.0 - */ -public interface RemoteEndpointAwareSSLOptions extends SSLOptions { - - /** - * Creates a new SSL handler for the given Netty channel and the given remote endpoint. - * - *

This gets called each time the driver opens a new connection to a Cassandra host. The newly - * created handler will be added to the channel's pipeline to provide SSL support for the - * connection. - * - *

You don't necessarily need to implement this method directly; see the provided - * implementations: {@link RemoteEndpointAwareJdkSSLOptions} and {@link - * RemoteEndpointAwareNettySSLOptions}. - * - * @param channel the channel. - * @param remoteEndpoint the remote endpoint address. - * @return a newly-created {@link SslHandler}. - */ - SslHandler newSSLHandler(SocketChannel channel, InetSocketAddress remoteEndpoint); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ReplicationFactor.java b/driver-core/src/main/java/com/datastax/driver/core/ReplicationFactor.java deleted file mode 100644 index a99e5c2f0e8..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ReplicationFactor.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -// This class is a subset of server version at org.apache.cassandra.locator.ReplicationFactor - -class ReplicationFactor { - private final int allReplicas; - private final int fullReplicas; - private final int transientReplicas; - - ReplicationFactor(int allReplicas, int transientReplicas) { - this.allReplicas = allReplicas; - this.transientReplicas = transientReplicas; - this.fullReplicas = allReplicas - transientReplicas; - } - - ReplicationFactor(int allReplicas) { - this(allReplicas, 0); - } - - int fullReplicas() { - return fullReplicas; - } - - int transientReplicas() { - return transientReplicas; - } - - boolean hasTransientReplicas() { - return transientReplicas > 0; - } - - static ReplicationFactor fromString(String s) { - if (s.contains("/")) { - int slash = s.indexOf('/'); - String allPart = s.substring(0, slash); - String transientPart = s.substring(slash + 1); - return new ReplicationFactor(Integer.parseInt(allPart), Integer.parseInt(transientPart)); - } else { - return new ReplicationFactor(Integer.parseInt(s), 0); - } - } - - @Override - public String toString() { - return allReplicas + (hasTransientReplicas() ? "/" + transientReplicas() : ""); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof ReplicationFactor)) { - return false; - } - ReplicationFactor that = (ReplicationFactor) o; - return allReplicas == that.allReplicas && transientReplicas == that.transientReplicas; - } - - @Override - public int hashCode() { - return allReplicas ^ transientReplicas; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ReplicationStategy.java b/driver-core/src/main/java/com/datastax/driver/core/ReplicationStategy.java deleted file mode 100644 index 06351604570..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ReplicationStategy.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/* - * Computes the token->list association, given the token ring and token->primary token map. - * - * Note: it's not an interface mainly because we don't want to expose it. - */ -abstract class ReplicationStrategy { - private static final Logger logger = LoggerFactory.getLogger(ReplicationStrategy.class); - - static ReplicationStrategy create(Map replicationOptions) { - - String strategyClass = replicationOptions.get("class"); - if (strategyClass == null) return null; - - try { - if (strategyClass.contains("SimpleStrategy")) { - String repFactorString = replicationOptions.get("replication_factor"); - return repFactorString == null - ? null - : new SimpleStrategy(ReplicationFactor.fromString(repFactorString)); - } else if (strategyClass.contains("NetworkTopologyStrategy")) { - Map dcRfs = new HashMap(); - for (Map.Entry entry : replicationOptions.entrySet()) { - if (entry.getKey().equals("class")) continue; - - dcRfs.put(entry.getKey(), ReplicationFactor.fromString(entry.getValue())); - } - return new NetworkTopologyStrategy(dcRfs); - } else { - // We might want to support oldNetworkTopologyStrategy, though not sure anyone still using - // that - return null; - } - } catch (NumberFormatException e) { - // Cassandra wouldn't let that pass in the first place so this really should never happen - logger.error("Failed to parse replication options: " + replicationOptions, e); - return null; - } - } - - abstract Map> computeTokenToReplicaMap( - String keyspaceName, Map tokenToPrimary, List ring); - - private static Token getTokenWrapping(int i, List ring) { - return ring.get(i % ring.size()); - } - - static class SimpleStrategy extends ReplicationStrategy { - - private final ReplicationFactor replicationFactor; - - private SimpleStrategy(ReplicationFactor replicationFactor) { - this.replicationFactor = replicationFactor; - } - - @Override - Map> computeTokenToReplicaMap( - String keyspaceName, Map tokenToPrimary, List ring) { - - int rf = Math.min(replicationFactor.fullReplicas(), ring.size()); - - Map> replicaMap = new HashMap>(tokenToPrimary.size()); - for (int i = 0; i < ring.size(); i++) { - // Consecutive sections of the ring can assigned to the same host - Set replicas = new LinkedHashSet(); - for (int j = 0; j < ring.size() && replicas.size() < rf; j++) - replicas.add(tokenToPrimary.get(getTokenWrapping(i + j, ring))); - replicaMap.put(ring.get(i), ImmutableSet.copyOf(replicas)); - } - return replicaMap; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - SimpleStrategy that = (SimpleStrategy) o; - - return replicationFactor.equals(that.replicationFactor); - } - - @Override - public int hashCode() { - return replicationFactor.hashCode(); - } - } - - static class NetworkTopologyStrategy extends ReplicationStrategy { - private static final Logger logger = LoggerFactory.getLogger(NetworkTopologyStrategy.class); - - private final Map replicationFactors; - - private NetworkTopologyStrategy(Map replicationFactors) { - this.replicationFactors = replicationFactors; - } - - @Override - Map> computeTokenToReplicaMap( - String keyspaceName, Map tokenToPrimary, List ring) { - - logger.debug("Computing token to replica map for keyspace: {}.", keyspaceName); - - // Track how long it takes to compute the token to replica map - long startTime = System.currentTimeMillis(); - - // This is essentially a copy of org.apache.cassandra.locator.NetworkTopologyStrategy - Map> racks = getRacksInDcs(tokenToPrimary.values()); - Map> replicaMap = new HashMap>(tokenToPrimary.size()); - Map dcHostCount = Maps.newHashMapWithExpectedSize(replicationFactors.size()); - Set warnedDcs = Sets.newHashSetWithExpectedSize(replicationFactors.size()); - // find maximum number of nodes in each DC - for (Host host : Sets.newHashSet(tokenToPrimary.values())) { - String dc = host.getDatacenter(); - if (dcHostCount.get(dc) == null) { - dcHostCount.put(dc, 0); - } - dcHostCount.put(dc, dcHostCount.get(dc) + 1); - } - for (int i = 0; i < ring.size(); i++) { - Map> allDcReplicas = new HashMap>(); - Map> seenRacks = new HashMap>(); - Map> skippedDcEndpoints = new HashMap>(); - for (String dc : replicationFactors.keySet()) { - allDcReplicas.put(dc, new HashSet()); - seenRacks.put(dc, new HashSet()); - skippedDcEndpoints.put(dc, new LinkedHashSet()); // preserve order - } - - // Preserve order - primary replica will be first - Set replicas = new LinkedHashSet(); - for (int j = 0; j < ring.size() && !allDone(allDcReplicas, dcHostCount); j++) { - Host h = tokenToPrimary.get(getTokenWrapping(i + j, ring)); - String dc = h.getDatacenter(); - if (dc == null || !allDcReplicas.containsKey(dc)) continue; - - Integer rf = replicationFactors.get(dc).fullReplicas(); - Set dcReplicas = allDcReplicas.get(dc); - if (rf == null || dcReplicas.size() >= rf) continue; - - String rack = h.getRack(); - // Check if we already visited all racks in dc - if (rack == null || seenRacks.get(dc).size() == racks.get(dc).size()) { - replicas.add(h); - dcReplicas.add(h); - } else { - // Is this a new rack? - if (seenRacks.get(dc).contains(rack)) { - skippedDcEndpoints.get(dc).add(h); - } else { - replicas.add(h); - dcReplicas.add(h); - seenRacks.get(dc).add(rack); - // If we've run out of distinct racks, add the nodes skipped so far - if (seenRacks.get(dc).size() == racks.get(dc).size()) { - Iterator skippedIt = skippedDcEndpoints.get(dc).iterator(); - while (skippedIt.hasNext() && dcReplicas.size() < rf) { - Host nextSkipped = skippedIt.next(); - replicas.add(nextSkipped); - dcReplicas.add(nextSkipped); - } - } - } - } - } - - // If we haven't found enough replicas after a whole trip around the ring, this probably - // means that the replication factors are broken. - // Warn the user because that leads to quadratic performance of this method (JAVA-702). - for (Map.Entry> entry : allDcReplicas.entrySet()) { - String dcName = entry.getKey(); - int expectedFactor = replicationFactors.get(dcName).fullReplicas(); - int achievedFactor = entry.getValue().size(); - if (achievedFactor < expectedFactor && !warnedDcs.contains(dcName)) { - logger.warn( - "Error while computing token map for keyspace {} with datacenter {}: " - + "could not achieve replication factor {} (found {} replicas only), " - + "check your keyspace replication settings.", - keyspaceName, - dcName, - expectedFactor, - achievedFactor); - // only warn once per DC - warnedDcs.add(dcName); - } - } - - replicaMap.put(ring.get(i), ImmutableSet.copyOf(replicas)); - } - - long duration = System.currentTimeMillis() - startTime; - logger.debug( - "Token to replica map computation for keyspace {} completed in {} milliseconds", - keyspaceName, - duration); - - return replicaMap; - } - - private boolean allDone(Map> map, Map dcHostCount) { - for (Map.Entry> entry : map.entrySet()) { - String dc = entry.getKey(); - int dcCount = dcHostCount.get(dc) == null ? 0 : dcHostCount.get(dc); - if (entry.getValue().size() < Math.min(replicationFactors.get(dc).fullReplicas(), dcCount)) - return false; - } - return true; - } - - private Map> getRacksInDcs(Iterable hosts) { - Map> result = new HashMap>(); - for (Host host : hosts) { - Set racks = result.get(host.getDatacenter()); - if (racks == null) { - racks = new HashSet(); - result.put(host.getDatacenter(), racks); - } - racks.add(host.getRack()); - } - return result; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - NetworkTopologyStrategy that = (NetworkTopologyStrategy) o; - - return replicationFactors.equals(that.replicationFactors); - } - - @Override - public int hashCode() { - return replicationFactors.hashCode(); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/RequestHandler.java b/driver-core/src/main/java/com/datastax/driver/core/RequestHandler.java deleted file mode 100644 index 3731b0949cd..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/RequestHandler.java +++ /dev/null @@ -1,1078 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.codahale.metrics.Timer; -import com.datastax.driver.core.exceptions.BootstrappingException; -import com.datastax.driver.core.exceptions.BusyConnectionException; -import com.datastax.driver.core.exceptions.BusyPoolException; -import com.datastax.driver.core.exceptions.ConnectionException; -import com.datastax.driver.core.exceptions.DriverException; -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.exceptions.NoHostAvailableException; -import com.datastax.driver.core.exceptions.OperationTimedOutException; -import com.datastax.driver.core.exceptions.OverloadedException; -import com.datastax.driver.core.exceptions.ReadFailureException; -import com.datastax.driver.core.exceptions.ReadTimeoutException; -import com.datastax.driver.core.exceptions.ServerError; -import com.datastax.driver.core.exceptions.UnavailableException; -import com.datastax.driver.core.exceptions.WriteFailureException; -import com.datastax.driver.core.exceptions.WriteTimeoutException; -import com.datastax.driver.core.policies.RetryPolicy; -import com.datastax.driver.core.policies.RetryPolicy.RetryDecision.Type; -import com.datastax.driver.core.policies.SpeculativeExecutionPolicy.SpeculativeExecutionPlan; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterators; -import com.google.common.collect.Sets; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.ListenableFuture; -import io.netty.util.Timeout; -import io.netty.util.TimerTask; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Handles a request to cassandra, dealing with host failover and retries on unavailable/timeout. - */ -class RequestHandler { - private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class); - - private static final boolean HOST_METRICS_ENABLED = - Boolean.getBoolean("com.datastax.driver.HOST_METRICS_ENABLED"); - private static final QueryLogger QUERY_LOGGER = QueryLogger.builder().build(); - static final String DISABLE_QUERY_WARNING_LOGS = "com.datastax.driver.DISABLE_QUERY_WARNING_LOGS"; - - final String id; - - private final SessionManager manager; - private final Callback callback; - - private final QueryPlan queryPlan; - private final SpeculativeExecutionPlan speculativeExecutionPlan; - private final boolean allowSpeculativeExecutions; - private final Set runningExecutions = Sets.newCopyOnWriteArraySet(); - private final Set scheduledExecutions = Sets.newCopyOnWriteArraySet(); - private final Statement statement; - private final io.netty.util.Timer scheduler; - - private volatile List triedHosts; - private volatile ConcurrentMap errors; - - private final Timer.Context timerContext; - private final long startTime; - - private final AtomicBoolean isDone = new AtomicBoolean(); - private final AtomicInteger executionIndex = new AtomicInteger(); - - public RequestHandler(SessionManager manager, Callback callback, Statement statement) { - this.id = Long.toString(System.identityHashCode(this)); - if (logger.isTraceEnabled()) logger.trace("[{}] {}", id, statement); - this.manager = manager; - this.callback = callback; - this.scheduler = manager.cluster.manager.connectionFactory.timer; - - callback.register(this); - - // If host is explicitly set on statement, bypass load balancing policy. - if (statement.getHost() != null) { - this.queryPlan = new QueryPlan(Iterators.singletonIterator(statement.getHost())); - } else { - this.queryPlan = - new QueryPlan( - manager.loadBalancingPolicy().newQueryPlan(manager.poolsState.keyspace, statement)); - } - - this.speculativeExecutionPlan = - manager.speculativeExecutionPolicy().newPlan(manager.poolsState.keyspace, statement); - this.allowSpeculativeExecutions = - statement != Statement.DEFAULT - && statement.isIdempotentWithDefault(manager.configuration().getQueryOptions()); - this.statement = statement; - - this.timerContext = metricsEnabled() ? metrics().getRequestsTimer().time() : null; - this.startTime = System.nanoTime(); - } - - void sendRequest() { - startNewExecution(); - } - - // Called when the corresponding ResultSetFuture is cancelled by the client - void cancel() { - if (!isDone.compareAndSet(false, true)) return; - - cancelPendingExecutions(null); - } - - private void startNewExecution() { - if (isDone.get()) return; - - Message.Request request = callback.request(); - int position = executionIndex.getAndIncrement(); - - SpeculativeExecution execution = new SpeculativeExecution(request, position); - runningExecutions.add(execution); - execution.findNextHostAndQuery(); - } - - private void scheduleExecution(long delayMillis) { - if (isDone.get() || delayMillis < 0) return; - if (logger.isTraceEnabled()) - logger.trace("[{}] Schedule next speculative execution in {} ms", id, delayMillis); - if (delayMillis == 0) { - // kick off request immediately - scheduleExecutionImmediately(); - } else { - scheduledExecutions.add( - scheduler.newTimeout(newExecutionTask, delayMillis, TimeUnit.MILLISECONDS)); - } - } - - private final TimerTask newExecutionTask = - new TimerTask() { - @Override - public void run(final Timeout timeout) throws Exception { - scheduledExecutions.remove(timeout); - if (!isDone.get()) { - // We're on the timer thread so reschedule to another executor - manager - .executor() - .execute( - new Runnable() { - @Override - public void run() { - scheduleExecutionImmediately(); - } - }); - } - } - }; - - private void scheduleExecutionImmediately() { - if (metricsEnabled()) metrics().getErrorMetrics().getSpeculativeExecutions().inc(); - startNewExecution(); - } - - private void cancelPendingExecutions(SpeculativeExecution ignore) { - for (SpeculativeExecution execution : runningExecutions) - if (execution != ignore) // not vital but this produces nicer logs - execution.cancel(); - for (Timeout execution : scheduledExecutions) execution.cancel(); - } - - private void setFinalResult( - SpeculativeExecution execution, Connection connection, Message.Response response) { - if (!isDone.compareAndSet(false, true)) { - if (logger.isTraceEnabled()) - logger.trace("[{}] Got beaten to setting the result", execution.id); - return; - } - - if (logger.isTraceEnabled()) logger.trace("[{}] Setting final result", execution.id); - - cancelPendingExecutions(execution); - - try { - if (timerContext != null) timerContext.stop(); - - ExecutionInfo info; - int speculativeExecutions = executionIndex.get() - 1; - // Avoid creating a new instance if we can reuse the host's default one - if (execution.position == 0 - && speculativeExecutions == 0 - && triedHosts == null - && execution.retryConsistencyLevel == null - && response.getCustomPayload() == null) { - info = execution.current.defaultExecutionInfo; - } else { - List hosts; - if (triedHosts == null) { - hosts = ImmutableList.of(execution.current); - } else { - hosts = triedHosts; - hosts.add(execution.current); - } - info = - new ExecutionInfo( - speculativeExecutions, - execution.position, - hosts, - execution.retryConsistencyLevel, - response.getCustomPayload()); - } - // if the response from the server has warnings, they'll be set on the ExecutionInfo. Log them - // here, unless they've been disabled. - if (response.warnings != null - && !response.warnings.isEmpty() - && !Boolean.getBoolean(RequestHandler.DISABLE_QUERY_WARNING_LOGS) - && logger.isWarnEnabled()) { - logServerWarnings(response.warnings); - } - callback.onSet(connection, response, info, statement, System.nanoTime() - startTime); - } catch (Exception e) { - callback.onException( - connection, - new DriverInternalError( - "Unexpected exception while setting final result from " + response, e), - System.nanoTime() - startTime, /*unused*/ - 0); - } - } - - private void logServerWarnings(List warnings) { - // truncate the statement query to the DEFAULT_MAX_QUERY_STRING_LENGTH, if necessary - final String queryString = QUERY_LOGGER.statementAsString(statement); - // log each warning separately - for (String warning : warnings) { - logger.warn("Query '{}' generated server side warning(s): {}", queryString, warning); - } - } - - private void setFinalException( - SpeculativeExecution execution, Connection connection, Exception exception) { - if (!isDone.compareAndSet(false, true)) { - if (logger.isTraceEnabled()) - logger.trace("[{}] Got beaten to setting final exception", execution.id); - return; - } - - if (logger.isTraceEnabled()) logger.trace("[{}] Setting final exception", execution.id); - - cancelPendingExecutions(execution); - - try { - if (timerContext != null) timerContext.stop(); - } finally { - callback.onException(connection, exception, System.nanoTime() - startTime, /*unused*/ 0); - } - } - - // Triggered when an execution reaches the end of the query plan. - // This is only a failure if there are no other running executions. - private void reportNoMoreHosts(SpeculativeExecution execution) { - runningExecutions.remove(execution); - if (runningExecutions.isEmpty()) - setFinalException( - execution, - null, - new NoHostAvailableException( - errors == null ? Collections.emptyMap() : errors)); - } - - private boolean metricsEnabled() { - return manager.configuration().getMetricsOptions().isEnabled(); - } - - private boolean hostMetricsEnabled() { - return HOST_METRICS_ENABLED && metricsEnabled(); - } - - private Metrics metrics() { - return manager.cluster.manager.metrics; - } - - private RetryPolicy retryPolicy() { - return statement.getRetryPolicy() == null - ? manager.configuration().getPolicies().getRetryPolicy() - : statement.getRetryPolicy(); - } - - interface Callback extends Connection.ResponseCallback { - void onSet( - Connection connection, - Message.Response response, - ExecutionInfo info, - Statement statement, - long latency); - - void register(RequestHandler handler); - } - - /** - * An execution of the query against the cluster. There is at least one instance per - * RequestHandler, and possibly more (depending on the SpeculativeExecutionPolicy). Each instance - * may retry on the same host, or on other hosts as defined by the RetryPolicy. All instances run - * concurrently and share the same query plan. There are three ways a SpeculativeExecution can - * stop: - it completes the query (with either a success or a fatal error), and reports the result - * to the RequestHandler - it gets cancelled, either because another execution completed the - * query, or because the RequestHandler was cancelled - it reaches the end of the query plan and - * informs the RequestHandler, which will decide what to do - */ - class SpeculativeExecution implements Connection.ResponseCallback { - final String id; - private final Message.Request request; - private final int position; - private volatile Host current; - private volatile ConsistencyLevel retryConsistencyLevel; - private final AtomicReference queryStateRef; - private final AtomicBoolean nextExecutionScheduled = new AtomicBoolean(); - private final long startTime = System.nanoTime(); - - // This represents the number of times a retry has been triggered by the RetryPolicy (this is - // different from - // queryStateRef.get().retryCount, because some retries don't involve the policy, for example - // after an - // UNPREPARED response). - // This is incremented by one writer at a time, so volatile is good enough. - private volatile int retriesByPolicy; - - private volatile Connection.ResponseHandler connectionHandler; - - SpeculativeExecution(Message.Request request, int position) { - this.id = RequestHandler.this.id + "-" + position; - this.request = request; - this.position = position; - this.queryStateRef = new AtomicReference(QueryState.INITIAL); - if (logger.isTraceEnabled()) logger.trace("[{}] Starting", id); - } - - void findNextHostAndQuery() { - try { - Host host; - while (!isDone.get() - && (host = queryPlan.next()) != null - && !queryStateRef.get().isCancelled()) { - if (query(host)) { - if (hostMetricsEnabled()) { - metrics().getRegistry().counter(MetricsUtil.hostMetricName("writes.", host)).inc(); - } - return; - } else if (hostMetricsEnabled()) { - metrics() - .getRegistry() - .counter(MetricsUtil.hostMetricName("write-errors.", host)) - .inc(); - } - } - if (current != null) { - if (triedHosts == null) triedHosts = new CopyOnWriteArrayList(); - triedHosts.add(current); - } - reportNoMoreHosts(this); - } catch (Exception e) { - // Shouldn't happen really, but if ever the loadbalancing policy returned iterator throws, - // we don't want to block. - setFinalException( - null, - new DriverInternalError("An unexpected error happened while sending requests", e)); - } - } - - private boolean query(final Host host) { - HostConnectionPool pool = manager.pools.get(host); - if (pool == null || pool.isClosed()) return false; - - if (logger.isTraceEnabled()) logger.trace("[{}] Querying node {}", id, host); - - if (allowSpeculativeExecutions && nextExecutionScheduled.compareAndSet(false, true)) - scheduleExecution(speculativeExecutionPlan.nextExecution(host)); - - PoolingOptions poolingOptions = manager.configuration().getPoolingOptions(); - ListenableFuture connectionFuture = - pool.borrowConnection( - poolingOptions.getPoolTimeoutMillis(), - TimeUnit.MILLISECONDS, - poolingOptions.getMaxQueueSize()); - GuavaCompatibility.INSTANCE.addCallback( - connectionFuture, - new FutureCallback() { - @Override - public void onSuccess(Connection connection) { - if (isDone.get()) { - connection.release(); - return; - } - if (current != null) { - if (triedHosts == null) triedHosts = new CopyOnWriteArrayList(); - triedHosts.add(current); - } - current = host; - try { - write(connection, SpeculativeExecution.this); - } catch (ConnectionException e) { - // If we have any problem with the connection, move to the next node. - if (metricsEnabled()) metrics().getErrorMetrics().getConnectionErrors().inc(); - if (connection != null) connection.release(); - logError(host.getEndPoint(), e); - findNextHostAndQuery(); - } catch (BusyConnectionException e) { - // The pool shouldn't have give us a busy connection unless we've maxed up the pool, - // so move on to the next host. - connection.release(true); - logError(host.getEndPoint(), e); - findNextHostAndQuery(); - } catch (RuntimeException e) { - if (connection != null) connection.release(); - logger.warn( - "Unexpected error while querying {} - [{}]. Find next host to query.", - host.getEndPoint(), - e.toString()); - logError(host.getEndPoint(), e); - findNextHostAndQuery(); - } - } - - @Override - public void onFailure(Throwable t) { - if (t instanceof BusyPoolException) { - logError(host.getEndPoint(), t); - } else { - logger.warn( - "Unexpected error while querying {} - [{}]. Find next host to query.", - host.getEndPoint(), - t.toString()); - logError(host.getEndPoint(), t); - } - findNextHostAndQuery(); - } - }); - return true; - } - - private void write(Connection connection, Connection.ResponseCallback responseCallback) - throws ConnectionException, BusyConnectionException { - // Make sure cancel() does not see a stale connectionHandler if it sees the new query state - // before connection.write has completed - connectionHandler = null; - - // Ensure query state is "in progress" (can be already if connection.write failed on a - // previous node and we're retrying) - while (true) { - QueryState previous = queryStateRef.get(); - if (previous.isCancelled()) { - connection.release(); - return; - } - if (previous.inProgress || queryStateRef.compareAndSet(previous, previous.startNext())) - break; - } - - connectionHandler = - connection.write(responseCallback, statement.getReadTimeoutMillis(), false); - // Only start the timeout when we're sure connectionHandler is set. This avoids an edge case - // where onTimeout() was triggered - // *before* the call to connection.write had returned. - connectionHandler.startTimeout(); - - // Note that we could have already received the response here (so onSet() / onException() - // would have been called). This is - // why we only test for CANCELLED_WHILE_IN_PROGRESS below. - - // If cancel() was called after we set the state to "in progress", but before connection.write - // had completed, it might have - // missed the new value of connectionHandler. So make sure that cancelHandler() gets called - // here (we might call it twice, - // but it knows how to deal with it). - if (queryStateRef.get() == QueryState.CANCELLED_WHILE_IN_PROGRESS - && connectionHandler.cancelHandler()) connection.release(); - } - - private RetryPolicy.RetryDecision computeRetryDecisionOnRequestError( - DriverException exception) { - RetryPolicy.RetryDecision decision; - if (statement.isIdempotentWithDefault(manager.cluster.getConfiguration().getQueryOptions())) { - decision = - retryPolicy() - .onRequestError(statement, request().consistency(), exception, retriesByPolicy); - } else { - decision = RetryPolicy.RetryDecision.rethrow(); - } - if (metricsEnabled()) { - if (exception instanceof OperationTimedOutException) { - metrics().getErrorMetrics().getClientTimeouts().inc(); - if (decision.getType() == Type.RETRY) - metrics().getErrorMetrics().getRetriesOnClientTimeout().inc(); - if (decision.getType() == Type.IGNORE) - metrics().getErrorMetrics().getIgnoresOnClientTimeout().inc(); - } else if (exception instanceof ConnectionException) { - metrics().getErrorMetrics().getConnectionErrors().inc(); - if (decision.getType() == Type.RETRY) - metrics().getErrorMetrics().getRetriesOnConnectionError().inc(); - if (decision.getType() == Type.IGNORE) - metrics().getErrorMetrics().getIgnoresOnConnectionError().inc(); - } else { - metrics().getErrorMetrics().getOthers().inc(); - if (decision.getType() == Type.RETRY) - metrics().getErrorMetrics().getRetriesOnOtherErrors().inc(); - if (decision.getType() == Type.IGNORE) - metrics().getErrorMetrics().getIgnoresOnOtherErrors().inc(); - } - } - return decision; - } - - private void processRetryDecision( - RetryPolicy.RetryDecision retryDecision, - Connection connection, - Exception exceptionToReport) { - switch (retryDecision.getType()) { - case RETRY: - retriesByPolicy++; - if (logger.isDebugEnabled()) - logger.debug( - "[{}] Doing retry {} for query {} at consistency {}", - id, - retriesByPolicy, - statement, - retryDecision.getRetryConsistencyLevel()); - if (metricsEnabled()) metrics().getErrorMetrics().getRetries().inc(); - // log error for the current host if we are switching to another one - if (!retryDecision.isRetryCurrent()) logError(connection.endPoint, exceptionToReport); - retry(retryDecision.isRetryCurrent(), retryDecision.getRetryConsistencyLevel()); - break; - case RETHROW: - setFinalException(connection, exceptionToReport); - break; - case IGNORE: - if (metricsEnabled()) metrics().getErrorMetrics().getIgnores().inc(); - setFinalResult(connection, new Responses.Result.Void()); - break; - } - } - - private void retry(final boolean retryCurrent, ConsistencyLevel newConsistencyLevel) { - final Host h = current; - if (newConsistencyLevel != null) this.retryConsistencyLevel = newConsistencyLevel; - - if (queryStateRef.get().isCancelled()) return; - - if (!retryCurrent || !query(h)) findNextHostAndQuery(); - } - - private void logError(EndPoint endPoint, Throwable exception) { - logger.debug("[{}] Error querying {} : {}", id, endPoint, exception.toString()); - if (errors == null) { - synchronized (RequestHandler.this) { - if (errors == null) { - errors = new ConcurrentHashMap(); - } - } - } - errors.put(endPoint, exception); - } - - void cancel() { - // Atomically set a special QueryState, that will cause any further operation to abort. - // We want to remember whether a request was in progress when we did this, so there are two - // cancel states. - while (true) { - QueryState previous = queryStateRef.get(); - if (previous.isCancelled()) { - return; - } else if (previous.inProgress - && queryStateRef.compareAndSet(previous, QueryState.CANCELLED_WHILE_IN_PROGRESS)) { - if (logger.isTraceEnabled()) logger.trace("[{}] Cancelled while in progress", id); - // The connectionHandler should be non-null, but we might miss the update if we're racing - // with write(). - // If it's still null, this will be handled by re-checking queryStateRef at the end of - // write(). - if (connectionHandler != null && connectionHandler.cancelHandler()) - connectionHandler.connection.release(); - Host queriedHost = current; - if (queriedHost != null && statement != Statement.DEFAULT) { - manager.cluster.manager.reportQuery( - queriedHost, - statement, - CancelledSpeculativeExecutionException.INSTANCE, - System.nanoTime() - startTime); - } - return; - } else if (!previous.inProgress - && queryStateRef.compareAndSet(previous, QueryState.CANCELLED_WHILE_COMPLETE)) { - if (logger.isTraceEnabled()) logger.trace("[{}] Cancelled while complete", id); - Host queriedHost = current; - if (queriedHost != null && statement != Statement.DEFAULT) { - manager.cluster.manager.reportQuery( - queriedHost, - statement, - CancelledSpeculativeExecutionException.INSTANCE, - System.nanoTime() - startTime); - } - return; - } - } - } - - @Override - public Message.Request request() { - if (retryConsistencyLevel != null && retryConsistencyLevel != request.consistency()) - return request.copy(retryConsistencyLevel); - else return request; - } - - @Override - public void onSet( - Connection connection, Message.Response response, long latency, int retryCount) { - QueryState queryState = queryStateRef.get(); - if (!queryState.isInProgressAt(retryCount) - || !queryStateRef.compareAndSet(queryState, queryState.complete())) { - logger.debug( - "onSet triggered but the response was completed by another thread, cancelling (retryCount = {}, queryState = {}, queryStateRef = {})", - retryCount, - queryState, - queryStateRef.get()); - return; - } - - Host queriedHost = current; - Exception exceptionToReport = null; - try { - switch (response.type) { - case RESULT: - connection.release(); - setFinalResult(connection, response); - break; - case ERROR: - Responses.Error err = (Responses.Error) response; - exceptionToReport = err.asException(connection.endPoint); - RetryPolicy.RetryDecision retry = null; - RetryPolicy retryPolicy = retryPolicy(); - switch (err.code) { - case READ_TIMEOUT: - connection.release(); - assert err.infos instanceof ReadTimeoutException; - ReadTimeoutException rte = (ReadTimeoutException) err.infos; - retry = - retryPolicy.onReadTimeout( - statement, - rte.getConsistencyLevel(), - rte.getRequiredAcknowledgements(), - rte.getReceivedAcknowledgements(), - rte.wasDataRetrieved(), - retriesByPolicy); - if (metricsEnabled()) { - metrics().getErrorMetrics().getReadTimeouts().inc(); - if (retry.getType() == Type.RETRY) - metrics().getErrorMetrics().getRetriesOnReadTimeout().inc(); - if (retry.getType() == Type.IGNORE) - metrics().getErrorMetrics().getIgnoresOnReadTimeout().inc(); - } - break; - case WRITE_TIMEOUT: - connection.release(); - assert err.infos instanceof WriteTimeoutException; - WriteTimeoutException wte = (WriteTimeoutException) err.infos; - if (statement.isIdempotentWithDefault( - manager.cluster.getConfiguration().getQueryOptions())) - retry = - retryPolicy.onWriteTimeout( - statement, - wte.getConsistencyLevel(), - wte.getWriteType(), - wte.getRequiredAcknowledgements(), - wte.getReceivedAcknowledgements(), - retriesByPolicy); - else { - retry = RetryPolicy.RetryDecision.rethrow(); - } - if (metricsEnabled()) { - metrics().getErrorMetrics().getWriteTimeouts().inc(); - if (retry.getType() == Type.RETRY) - metrics().getErrorMetrics().getRetriesOnWriteTimeout().inc(); - if (retry.getType() == Type.IGNORE) - metrics().getErrorMetrics().getIgnoresOnWriteTimeout().inc(); - } - break; - case UNAVAILABLE: - connection.release(); - assert err.infos instanceof UnavailableException; - UnavailableException ue = (UnavailableException) err.infos; - retry = - retryPolicy.onUnavailable( - statement, - ue.getConsistencyLevel(), - ue.getRequiredReplicas(), - ue.getAliveReplicas(), - retriesByPolicy); - if (metricsEnabled()) { - metrics().getErrorMetrics().getUnavailables().inc(); - if (retry.getType() == Type.RETRY) - metrics().getErrorMetrics().getRetriesOnUnavailable().inc(); - if (retry.getType() == Type.IGNORE) - metrics().getErrorMetrics().getIgnoresOnUnavailable().inc(); - } - break; - case OVERLOADED: - connection.release(); - assert exceptionToReport instanceof OverloadedException; - logger.warn("Host {} is overloaded.", connection.endPoint); - retry = computeRetryDecisionOnRequestError((OverloadedException) exceptionToReport); - break; - case SERVER_ERROR: - connection.release(); - assert exceptionToReport instanceof ServerError; - logger.warn( - "{} replied with server error ({}), defuncting connection.", - connection.endPoint, - err.message); - // Defunct connection - connection.defunct(exceptionToReport); - retry = computeRetryDecisionOnRequestError((ServerError) exceptionToReport); - break; - case IS_BOOTSTRAPPING: - connection.release(); - assert exceptionToReport instanceof BootstrappingException; - logger.error( - "Query sent to {} but it is bootstrapping. This shouldn't happen but trying next host.", - connection.endPoint); - if (metricsEnabled()) { - metrics().getErrorMetrics().getOthers().inc(); - } - logError(connection.endPoint, exceptionToReport); - retry(false, null); - return; - case UNPREPARED: - // Do not release connection yet, because we might reuse it to send the PREPARE - // message (see write() call below) - assert err.infos instanceof MD5Digest; - MD5Digest id = (MD5Digest) err.infos; - PreparedStatement toPrepare = manager.cluster.manager.preparedQueries.get(id); - if (toPrepare == null) { - // This shouldn't happen - connection.release(); - String msg = String.format("Tried to execute unknown prepared query %s", id); - logger.error(msg); - setFinalException(connection, new DriverInternalError(msg)); - return; - } - - String currentKeyspace = connection.keyspace(); - String prepareKeyspace = toPrepare.getQueryKeyspace(); - if (prepareKeyspace != null - && (currentKeyspace == null || !currentKeyspace.equals(prepareKeyspace))) { - // This shouldn't happen in normal use, because a user shouldn't try to execute - // a prepared statement with the wrong keyspace set. - // Fail fast (we can't change the keyspace to reprepare, because we're using a - // pooled connection - // that's shared with other requests). - connection.release(); - throw new IllegalStateException( - String.format( - "Statement was prepared on keyspace %s, can't execute it on %s (%s)", - toPrepare.getQueryKeyspace(), - connection.keyspace(), - toPrepare.getQueryString())); - } - - logger.info( - "Query {} is not prepared on {}, preparing before retrying executing. " - + "Seeing this message a few times is fine, but seeing it a lot may be source of performance problems", - toPrepare.getQueryString(), - toPrepare.getQueryKeyspace(), - connection.endPoint); - - write(connection, prepareAndRetry(toPrepare.getQueryString())); - // we're done for now, the prepareAndRetry callback will handle the rest - return; - case READ_FAILURE: - assert exceptionToReport instanceof ReadFailureException; - connection.release(); - retry = - computeRetryDecisionOnRequestError((ReadFailureException) exceptionToReport); - break; - case WRITE_FAILURE: - assert exceptionToReport instanceof WriteFailureException; - connection.release(); - if (statement.isIdempotentWithDefault( - manager.cluster.getConfiguration().getQueryOptions())) { - retry = - computeRetryDecisionOnRequestError((WriteFailureException) exceptionToReport); - } else { - retry = RetryPolicy.RetryDecision.rethrow(); - } - break; - default: - connection.release(); - if (metricsEnabled()) metrics().getErrorMetrics().getOthers().inc(); - break; - } - - if (retry == null) setFinalResult(connection, response); - else { - processRetryDecision(retry, connection, exceptionToReport); - } - break; - default: - connection.release(); - setFinalResult(connection, response); - break; - } - } catch (Exception e) { - exceptionToReport = e; - setFinalException(connection, e); - } finally { - if (queriedHost != null && statement != Statement.DEFAULT) { - manager.cluster.manager.reportQuery(queriedHost, statement, exceptionToReport, latency); - } - } - } - - private Connection.ResponseCallback prepareAndRetry(final String toPrepare) { - // do not bother inspecting retry policy at this step, no other decision - // makes sense than retry on the same host if the query was prepared, - // or on another host, if an error/timeout occurred. - // The original request hasn't been executed so far, so there is no risk - // of re-executing non-idempotent statements. - return new Connection.ResponseCallback() { - - @Override - public Message.Request request() { - Requests.Prepare request = new Requests.Prepare(toPrepare); - // propagate the original custom payload in the prepare request - request.setCustomPayload(statement.getOutgoingPayload()); - return request; - } - - @Override - public int retryCount() { - return SpeculativeExecution.this.retryCount(); - } - - @Override - public void onSet( - Connection connection, Message.Response response, long latency, int retryCount) { - QueryState queryState = queryStateRef.get(); - if (!queryState.isInProgressAt(retryCount) - || !queryStateRef.compareAndSet(queryState, queryState.complete())) { - logger.debug( - "onSet triggered but the response was completed by another thread, cancelling (retryCount = {}, queryState = {}, queryStateRef = {})", - retryCount, - queryState, - queryStateRef.get()); - return; - } - - connection.release(); - - switch (response.type) { - case RESULT: - if (((Responses.Result) response).kind == Responses.Result.Kind.PREPARED) { - logger.debug("Scheduling retry now that query is prepared"); - retry(true, null); - } else { - logError( - connection.endPoint, - new DriverException("Got unexpected response to prepare message: " + response)); - retry(false, null); - } - break; - case ERROR: - logError( - connection.endPoint, - new DriverException("Error preparing query, got " + response)); - if (metricsEnabled()) metrics().getErrorMetrics().getOthers().inc(); - retry(false, null); - break; - default: - // Something's wrong, so we return but we let setFinalResult propagate the exception - SpeculativeExecution.this.setFinalResult(connection, response); - break; - } - } - - @Override - public void onException( - Connection connection, Exception exception, long latency, int retryCount) { - SpeculativeExecution.this.onException(connection, exception, latency, retryCount); - } - - @Override - public boolean onTimeout(Connection connection, long latency, int retryCount) { - QueryState queryState = queryStateRef.get(); - if (!queryState.isInProgressAt(retryCount) - || !queryStateRef.compareAndSet(queryState, queryState.complete())) { - logger.debug( - "onTimeout triggered but the response was completed by another thread, cancelling (retryCount = {}, queryState = {}, queryStateRef = {})", - retryCount, - queryState, - queryStateRef.get()); - return false; - } - connection.release(); - logError( - connection.endPoint, - new OperationTimedOutException( - connection.endPoint, "Timed out waiting for response to PREPARE message")); - retry(false, null); - return true; - } - }; - } - - @Override - public void onException( - Connection connection, Exception exception, long latency, int retryCount) { - QueryState queryState = queryStateRef.get(); - if (!queryState.isInProgressAt(retryCount) - || !queryStateRef.compareAndSet(queryState, queryState.complete())) { - logger.debug( - "onException triggered but the response was completed by another thread, cancelling (retryCount = {}, queryState = {}, queryStateRef = {})", - retryCount, - queryState, - queryStateRef.get()); - return; - } - - Host queriedHost = current; - try { - connection.release(); - - if (exception instanceof ConnectionException) { - RetryPolicy.RetryDecision decision = - computeRetryDecisionOnRequestError((ConnectionException) exception); - processRetryDecision(decision, connection, exception); - return; - } - setFinalException(connection, exception); - } catch (Exception e) { - // This shouldn't happen, but if it does, we want to signal the callback, not let it hang - // indefinitely - setFinalException( - null, - new DriverInternalError( - "An unexpected error happened while handling exception " + exception, e)); - } finally { - if (queriedHost != null && statement != Statement.DEFAULT) - manager.cluster.manager.reportQuery(queriedHost, statement, exception, latency); - } - } - - @Override - public boolean onTimeout(Connection connection, long latency, int retryCount) { - QueryState queryState = queryStateRef.get(); - if (!queryState.isInProgressAt(retryCount) - || !queryStateRef.compareAndSet(queryState, queryState.complete())) { - logger.debug( - "onTimeout triggered but the response was completed by another thread, cancelling (retryCount = {}, queryState = {}, queryStateRef = {})", - retryCount, - queryState, - queryStateRef.get()); - return false; - } - - Host queriedHost = current; - - OperationTimedOutException timeoutException = - new OperationTimedOutException( - connection.endPoint, "Timed out waiting for server response"); - - try { - connection.release(); - - RetryPolicy.RetryDecision decision = computeRetryDecisionOnRequestError(timeoutException); - processRetryDecision(decision, connection, timeoutException); - } catch (Exception e) { - // This shouldn't happen, but if it does, we want to signal the callback, not let it hang - // indefinitely - setFinalException( - null, - new DriverInternalError("An unexpected error happened while handling timeout", e)); - } finally { - if (queriedHost != null && statement != Statement.DEFAULT) - manager.cluster.manager.reportQuery(queriedHost, statement, timeoutException, latency); - } - return true; - } - - @Override - public int retryCount() { - return queryStateRef.get().retryCount; - } - - private void setFinalException(Connection connection, Exception exception) { - RequestHandler.this.setFinalException(this, connection, exception); - } - - private void setFinalResult(Connection connection, Message.Response response) { - RequestHandler.this.setFinalResult(this, connection, response); - } - } - - /** - * The state of a SpeculativeExecution. - * - *

This is used to prevent races between request completion (either success or error) and - * timeout. A retry is in progress once we have written the request to the connection and until we - * get back a response (see onSet or onException) or a timeout (see onTimeout). The count - * increments on each retry. - */ - static class QueryState { - static final QueryState INITIAL = new QueryState(-1, false); - static final QueryState CANCELLED_WHILE_IN_PROGRESS = new QueryState(Integer.MIN_VALUE, false); - static final QueryState CANCELLED_WHILE_COMPLETE = new QueryState(Integer.MIN_VALUE + 1, false); - - final int retryCount; - final boolean inProgress; - - private QueryState(int count, boolean inProgress) { - this.retryCount = count; - this.inProgress = inProgress; - } - - boolean isInProgressAt(int retryCount) { - return inProgress && this.retryCount == retryCount; - } - - QueryState complete() { - assert inProgress; - return new QueryState(retryCount, false); - } - - QueryState startNext() { - assert !inProgress; - return new QueryState(retryCount + 1, true); - } - - public boolean isCancelled() { - return this == CANCELLED_WHILE_IN_PROGRESS || this == CANCELLED_WHILE_COMPLETE; - } - - @Override - public String toString() { - return String.format( - "QueryState(count=%d, inProgress=%s, cancelled=%s)", - retryCount, inProgress, isCancelled()); - } - } - - /** - * Wraps the iterator return by {@link com.datastax.driver.core.policies.LoadBalancingPolicy} to - * make it safe for concurrent access by multiple threads. - */ - static class QueryPlan { - private final Iterator iterator; - - QueryPlan(Iterator iterator) { - this.iterator = iterator; - } - - /** @return null if there are no more hosts */ - synchronized Host next() { - return iterator.hasNext() ? iterator.next() : null; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Requests.java b/driver-core/src/main/java/com/datastax/driver/core/Requests.java deleted file mode 100644 index c9d8110c758..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Requests.java +++ /dev/null @@ -1,752 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import io.netty.buffer.ByteBuf; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; - -class Requests { - - static final ByteBuffer[] EMPTY_BB_ARRAY = new ByteBuffer[0]; - - private Requests() {} - - static class Startup extends Message.Request { - private static final String CQL_VERSION_OPTION = "CQL_VERSION"; - private static final String CQL_VERSION = "3.0.0"; - private static final String DRIVER_VERSION_OPTION = "DRIVER_VERSION"; - private static final String DRIVER_NAME_OPTION = "DRIVER_NAME"; - private static final String DRIVER_NAME = "DataStax Java Driver"; - - static final String COMPRESSION_OPTION = "COMPRESSION"; - static final String NO_COMPACT_OPTION = "NO_COMPACT"; - - static final Message.Coder coder = - new Message.Coder() { - @Override - public void encode(Startup msg, ByteBuf dest, ProtocolVersion version) { - CBUtil.writeStringMap(msg.options, dest); - } - - @Override - public int encodedSize(Startup msg, ProtocolVersion version) { - return CBUtil.sizeOfStringMap(msg.options); - } - }; - - private final Map options; - private final ProtocolOptions.Compression compression; - private final boolean noCompact; - - Startup(ProtocolOptions.Compression compression, boolean noCompact) { - super(Message.Request.Type.STARTUP); - this.compression = compression; - this.noCompact = noCompact; - - ImmutableMap.Builder map = new ImmutableMap.Builder(); - map.put(CQL_VERSION_OPTION, CQL_VERSION); - if (compression != ProtocolOptions.Compression.NONE) - map.put(COMPRESSION_OPTION, compression.toString()); - if (noCompact) map.put(NO_COMPACT_OPTION, "true"); - - map.put(DRIVER_VERSION_OPTION, Cluster.getDriverVersion()); - map.put(DRIVER_NAME_OPTION, DRIVER_NAME); - - this.options = map.build(); - } - - @Override - protected Request copyInternal() { - return new Startup(compression, noCompact); - } - - @Override - public String toString() { - return "STARTUP " + options; - } - } - - // Only for protocol v1 - static class Credentials extends Message.Request { - - static final Message.Coder coder = - new Message.Coder() { - - @Override - public void encode(Credentials msg, ByteBuf dest, ProtocolVersion version) { - assert version == ProtocolVersion.V1; - CBUtil.writeStringMap(msg.credentials, dest); - } - - @Override - public int encodedSize(Credentials msg, ProtocolVersion version) { - assert version == ProtocolVersion.V1; - return CBUtil.sizeOfStringMap(msg.credentials); - } - }; - - private final Map credentials; - - Credentials(Map credentials) { - super(Message.Request.Type.CREDENTIALS); - this.credentials = credentials; - } - - @Override - protected Request copyInternal() { - return new Credentials(credentials); - } - } - - static class Options extends Message.Request { - - static final Message.Coder coder = - new Message.Coder() { - @Override - public void encode(Options msg, ByteBuf dest, ProtocolVersion version) {} - - @Override - public int encodedSize(Options msg, ProtocolVersion version) { - return 0; - } - }; - - Options() { - super(Message.Request.Type.OPTIONS); - } - - @Override - protected Request copyInternal() { - return new Options(); - } - - @Override - public String toString() { - return "OPTIONS"; - } - } - - static class Query extends Message.Request { - - static final Message.Coder coder = - new Message.Coder() { - @Override - public void encode(Query msg, ByteBuf dest, ProtocolVersion version) { - CBUtil.writeLongString(msg.query, dest); - msg.options.encode(dest, version); - } - - @Override - public int encodedSize(Query msg, ProtocolVersion version) { - return CBUtil.sizeOfLongString(msg.query) + msg.options.encodedSize(version); - } - }; - - final String query; - final QueryProtocolOptions options; - - Query(String query) { - this(query, QueryProtocolOptions.DEFAULT, false); - } - - Query(String query, QueryProtocolOptions options, boolean tracingRequested) { - super(Type.QUERY, tracingRequested); - this.query = query; - this.options = options; - } - - @Override - protected Request copyInternal() { - return new Query(this.query, options, isTracingRequested()); - } - - @Override - protected Request copyInternal(ConsistencyLevel newConsistencyLevel) { - return new Query(this.query, options.copy(newConsistencyLevel), isTracingRequested()); - } - - @Override - public String toString() { - return "QUERY " + query + '(' + options + ')'; - } - } - - static class Execute extends Message.Request { - - static final Message.Coder coder = - new Message.Coder() { - @Override - public void encode(Execute msg, ByteBuf dest, ProtocolVersion version) { - CBUtil.writeShortBytes(msg.statementId.bytes, dest); - if (ProtocolFeature.PREPARED_METADATA_CHANGES.isSupportedBy(version)) - CBUtil.writeShortBytes(msg.resultMetadataId.bytes, dest); - msg.options.encode(dest, version); - } - - @Override - public int encodedSize(Execute msg, ProtocolVersion version) { - int size = CBUtil.sizeOfShortBytes(msg.statementId.bytes); - if (ProtocolFeature.PREPARED_METADATA_CHANGES.isSupportedBy(version)) - size += CBUtil.sizeOfShortBytes(msg.resultMetadataId.bytes); - size += msg.options.encodedSize(version); - return size; - } - }; - - final MD5Digest statementId; - final MD5Digest resultMetadataId; - final QueryProtocolOptions options; - - Execute( - MD5Digest statementId, - MD5Digest resultMetadataId, - QueryProtocolOptions options, - boolean tracingRequested) { - super(Message.Request.Type.EXECUTE, tracingRequested); - this.statementId = statementId; - this.resultMetadataId = resultMetadataId; - this.options = options; - } - - @Override - protected Request copyInternal() { - return new Execute(statementId, resultMetadataId, options, isTracingRequested()); - } - - @Override - protected Request copyInternal(ConsistencyLevel newConsistencyLevel) { - return new Execute( - statementId, resultMetadataId, options.copy(newConsistencyLevel), isTracingRequested()); - } - - @Override - public String toString() { - if (resultMetadataId != null) - return "EXECUTE preparedId: " - + statementId - + " resultMetadataId: " - + resultMetadataId - + " (" - + options - + ')'; - else return "EXECUTE preparedId: " + statementId + " (" + options + ')'; - } - } - - enum QueryFlag { - VALUES(0x00000001), - SKIP_METADATA(0x00000002), - PAGE_SIZE(0x00000004), - PAGING_STATE(0x00000008), - SERIAL_CONSISTENCY(0x00000010), - DEFAULT_TIMESTAMP(0x00000020), - VALUE_NAMES(0x00000040), - NOW_IN_SECONDS(0x00000100), - ; - - private int mask; - - QueryFlag(int mask) { - this.mask = mask; - } - - static EnumSet deserialize(int flags) { - EnumSet set = EnumSet.noneOf(QueryFlag.class); - for (QueryFlag flag : values()) { - if ((flags & flag.mask) != 0) set.add(flag); - } - return set; - } - - static void serialize(EnumSet flags, ByteBuf dest, ProtocolVersion version) { - int i = 0; - for (QueryFlag flag : flags) i |= flag.mask; - if (version.compareTo(ProtocolVersion.V5) >= 0) { - dest.writeInt(i); - } else { - dest.writeByte((byte) i); - } - } - - static int serializedSize(ProtocolVersion version) { - return version.compareTo(ProtocolVersion.V5) >= 0 ? 4 : 1; - } - } - - static class QueryProtocolOptions { - - static final QueryProtocolOptions DEFAULT = - new QueryProtocolOptions( - Message.Request.Type.QUERY, - ConsistencyLevel.ONE, - EMPTY_BB_ARRAY, - Collections.emptyMap(), - false, - -1, - null, - ConsistencyLevel.SERIAL, - Long.MIN_VALUE, - Integer.MIN_VALUE); - - private final EnumSet flags = EnumSet.noneOf(QueryFlag.class); - private final Message.Request.Type requestType; - final ConsistencyLevel consistency; - final ByteBuffer[] positionalValues; - final Map namedValues; - final boolean skipMetadata; - final int pageSize; - final ByteBuffer pagingState; - final ConsistencyLevel serialConsistency; - final long defaultTimestamp; - final int nowInSeconds; - - QueryProtocolOptions( - Message.Request.Type requestType, - ConsistencyLevel consistency, - ByteBuffer[] positionalValues, - Map namedValues, - boolean skipMetadata, - int pageSize, - ByteBuffer pagingState, - ConsistencyLevel serialConsistency, - long defaultTimestamp, - int nowInSeconds) { - - Preconditions.checkArgument(positionalValues.length == 0 || namedValues.isEmpty()); - - this.requestType = requestType; - this.consistency = consistency; - this.positionalValues = positionalValues; - this.namedValues = namedValues; - this.skipMetadata = skipMetadata; - this.pageSize = pageSize; - this.pagingState = pagingState; - this.serialConsistency = serialConsistency; - this.defaultTimestamp = defaultTimestamp; - this.nowInSeconds = nowInSeconds; - - // Populate flags - if (positionalValues.length > 0) { - flags.add(QueryFlag.VALUES); - } - if (!namedValues.isEmpty()) { - flags.add(QueryFlag.VALUES); - flags.add(QueryFlag.VALUE_NAMES); - } - if (skipMetadata) flags.add(QueryFlag.SKIP_METADATA); - if (pageSize >= 0) flags.add(QueryFlag.PAGE_SIZE); - if (pagingState != null) flags.add(QueryFlag.PAGING_STATE); - if (serialConsistency != ConsistencyLevel.SERIAL) flags.add(QueryFlag.SERIAL_CONSISTENCY); - if (defaultTimestamp != Long.MIN_VALUE) flags.add(QueryFlag.DEFAULT_TIMESTAMP); - if (nowInSeconds != Integer.MIN_VALUE) flags.add(QueryFlag.NOW_IN_SECONDS); - } - - QueryProtocolOptions copy(ConsistencyLevel newConsistencyLevel) { - return new QueryProtocolOptions( - requestType, - newConsistencyLevel, - positionalValues, - namedValues, - skipMetadata, - pageSize, - pagingState, - serialConsistency, - defaultTimestamp, - nowInSeconds); - } - - void encode(ByteBuf dest, ProtocolVersion version) { - switch (version) { - case V1: - // only EXECUTE messages have variables in V1, and their list must be written - // even if it is empty; and they are never named - if (requestType == Message.Request.Type.EXECUTE) - CBUtil.writeValueList(positionalValues, dest); - CBUtil.writeConsistencyLevel(consistency, dest); - break; - case V2: - case V3: - case V4: - case V5: - case V6: - CBUtil.writeConsistencyLevel(consistency, dest); - QueryFlag.serialize(flags, dest, version); - if (flags.contains(QueryFlag.VALUES)) { - if (flags.contains(QueryFlag.VALUE_NAMES)) { - assert version.compareTo(ProtocolVersion.V3) >= 0; - CBUtil.writeNamedValueList(namedValues, dest); - } else { - CBUtil.writeValueList(positionalValues, dest); - } - } - if (flags.contains(QueryFlag.PAGE_SIZE)) dest.writeInt(pageSize); - if (flags.contains(QueryFlag.PAGING_STATE)) CBUtil.writeValue(pagingState, dest); - if (flags.contains(QueryFlag.SERIAL_CONSISTENCY)) - CBUtil.writeConsistencyLevel(serialConsistency, dest); - if (version.compareTo(ProtocolVersion.V3) >= 0 - && flags.contains(QueryFlag.DEFAULT_TIMESTAMP)) dest.writeLong(defaultTimestamp); - if (version.compareTo(ProtocolVersion.V5) >= 0 - && flags.contains(QueryFlag.NOW_IN_SECONDS)) dest.writeInt(nowInSeconds); - break; - default: - throw version.unsupported(); - } - } - - int encodedSize(ProtocolVersion version) { - switch (version) { - case V1: - // only EXECUTE messages have variables in V1, and their list must be written - // even if it is empty; and they are never named - return (requestType == Message.Request.Type.EXECUTE - ? CBUtil.sizeOfValueList(positionalValues) - : 0) - + CBUtil.sizeOfConsistencyLevel(consistency); - case V2: - case V3: - case V4: - case V5: - case V6: - int size = 0; - size += CBUtil.sizeOfConsistencyLevel(consistency); - size += QueryFlag.serializedSize(version); - if (flags.contains(QueryFlag.VALUES)) { - if (flags.contains(QueryFlag.VALUE_NAMES)) { - assert version.compareTo(ProtocolVersion.V3) >= 0; - size += CBUtil.sizeOfNamedValueList(namedValues); - } else { - size += CBUtil.sizeOfValueList(positionalValues); - } - } - if (flags.contains(QueryFlag.PAGE_SIZE)) size += 4; - if (flags.contains(QueryFlag.PAGING_STATE)) size += CBUtil.sizeOfValue(pagingState); - if (flags.contains(QueryFlag.SERIAL_CONSISTENCY)) - size += CBUtil.sizeOfConsistencyLevel(serialConsistency); - if (version.compareTo(ProtocolVersion.V3) >= 0 - && flags.contains(QueryFlag.DEFAULT_TIMESTAMP)) size += 8; - if (version.compareTo(ProtocolVersion.V5) >= 0 - && flags.contains(QueryFlag.NOW_IN_SECONDS)) size += 4; - return size; - default: - throw version.unsupported(); - } - } - - @Override - public String toString() { - return String.format( - "[cl=%s, positionalVals=%s, namedVals=%s, skip=%b, psize=%d, state=%s, serialCl=%s]", - consistency, - Arrays.toString(positionalValues), - namedValues, - skipMetadata, - pageSize, - pagingState, - serialConsistency); - } - } - - static class Batch extends Message.Request { - - static final Message.Coder coder = - new Message.Coder() { - @Override - public void encode(Batch msg, ByteBuf dest, ProtocolVersion version) { - int queries = msg.queryOrIdList.size(); - assert queries <= 0xFFFF; - - dest.writeByte(fromType(msg.type)); - dest.writeShort(queries); - - for (int i = 0; i < queries; i++) { - Object q = msg.queryOrIdList.get(i); - dest.writeByte((byte) (q instanceof String ? 0 : 1)); - if (q instanceof String) CBUtil.writeLongString((String) q, dest); - else CBUtil.writeShortBytes(((MD5Digest) q).bytes, dest); - - CBUtil.writeValueList(msg.values[i], dest); - } - - msg.options.encode(dest, version); - } - - @Override - public int encodedSize(Batch msg, ProtocolVersion version) { - int size = 3; // type + nb queries - for (int i = 0; i < msg.queryOrIdList.size(); i++) { - Object q = msg.queryOrIdList.get(i); - size += - 1 - + (q instanceof String - ? CBUtil.sizeOfLongString((String) q) - : CBUtil.sizeOfShortBytes(((MD5Digest) q).bytes)); - - size += CBUtil.sizeOfValueList(msg.values[i]); - } - size += msg.options.encodedSize(version); - return size; - } - - private byte fromType(BatchStatement.Type type) { - switch (type) { - case LOGGED: - return 0; - case UNLOGGED: - return 1; - case COUNTER: - return 2; - default: - throw new AssertionError(); - } - } - }; - - final BatchStatement.Type type; - final List queryOrIdList; - final ByteBuffer[][] values; - final BatchProtocolOptions options; - - Batch( - BatchStatement.Type type, - List queryOrIdList, - ByteBuffer[][] values, - BatchProtocolOptions options, - boolean tracingRequested) { - super(Message.Request.Type.BATCH, tracingRequested); - this.type = type; - this.queryOrIdList = queryOrIdList; - this.values = values; - this.options = options; - } - - @Override - protected Request copyInternal() { - return new Batch(type, queryOrIdList, values, options, isTracingRequested()); - } - - @Override - protected Request copyInternal(ConsistencyLevel newConsistencyLevel) { - return new Batch( - type, queryOrIdList, values, options.copy(newConsistencyLevel), isTracingRequested()); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("BATCH of ["); - for (int i = 0; i < queryOrIdList.size(); i++) { - if (i > 0) sb.append(", "); - sb.append(queryOrIdList.get(i)).append(" with ").append(values[i].length).append(" values"); - } - sb.append("] with options ").append(options); - return sb.toString(); - } - } - - static class BatchProtocolOptions { - private final EnumSet flags = EnumSet.noneOf(QueryFlag.class); - final ConsistencyLevel consistency; - final ConsistencyLevel serialConsistency; - final long defaultTimestamp; - final int nowInSeconds; - - BatchProtocolOptions( - ConsistencyLevel consistency, - ConsistencyLevel serialConsistency, - long defaultTimestamp, - int nowInSeconds) { - this.consistency = consistency; - this.serialConsistency = serialConsistency; - this.defaultTimestamp = defaultTimestamp; - this.nowInSeconds = nowInSeconds; - - if (serialConsistency != ConsistencyLevel.SERIAL) flags.add(QueryFlag.SERIAL_CONSISTENCY); - if (defaultTimestamp != Long.MIN_VALUE) flags.add(QueryFlag.DEFAULT_TIMESTAMP); - if (nowInSeconds != Integer.MIN_VALUE) flags.add(QueryFlag.NOW_IN_SECONDS); - } - - BatchProtocolOptions copy(ConsistencyLevel newConsistencyLevel) { - return new BatchProtocolOptions( - newConsistencyLevel, serialConsistency, defaultTimestamp, nowInSeconds); - } - - void encode(ByteBuf dest, ProtocolVersion version) { - switch (version) { - case V2: - CBUtil.writeConsistencyLevel(consistency, dest); - break; - case V3: - case V4: - case V5: - case V6: - CBUtil.writeConsistencyLevel(consistency, dest); - QueryFlag.serialize(flags, dest, version); - if (flags.contains(QueryFlag.SERIAL_CONSISTENCY)) - CBUtil.writeConsistencyLevel(serialConsistency, dest); - if (flags.contains(QueryFlag.DEFAULT_TIMESTAMP)) dest.writeLong(defaultTimestamp); - if (version.compareTo(ProtocolVersion.V5) >= 0 - && flags.contains(QueryFlag.NOW_IN_SECONDS)) dest.writeInt(nowInSeconds); - break; - default: - throw version.unsupported(); - } - } - - int encodedSize(ProtocolVersion version) { - switch (version) { - case V2: - return CBUtil.sizeOfConsistencyLevel(consistency); - case V3: - case V4: - case V5: - case V6: - int size = 0; - size += CBUtil.sizeOfConsistencyLevel(consistency); - size += QueryFlag.serializedSize(version); - if (flags.contains(QueryFlag.SERIAL_CONSISTENCY)) - size += CBUtil.sizeOfConsistencyLevel(serialConsistency); - if (flags.contains(QueryFlag.DEFAULT_TIMESTAMP)) size += 8; - if (version.compareTo(ProtocolVersion.V5) >= 0 - && flags.contains(QueryFlag.NOW_IN_SECONDS)) size += 4; - return size; - default: - throw version.unsupported(); - } - } - - @Override - public String toString() { - return String.format( - "[cl=%s, serialCl=%s, defaultTs=%d]", consistency, serialConsistency, defaultTimestamp); - } - } - - static class Prepare extends Message.Request { - - static final Message.Coder coder = - new Message.Coder() { - - @Override - public void encode(Prepare msg, ByteBuf dest, ProtocolVersion version) { - CBUtil.writeLongString(msg.query, dest); - - if (version.compareTo(ProtocolVersion.V5) >= 0) { - // Write empty flags for now, to communicate that no keyspace is being set. - dest.writeInt(0); - } - } - - @Override - public int encodedSize(Prepare msg, ProtocolVersion version) { - int size = CBUtil.sizeOfLongString(msg.query); - - if (version.compareTo(ProtocolVersion.V5) >= 0) { - size += 4; // flags - } - return size; - } - }; - - private final String query; - - Prepare(String query) { - super(Message.Request.Type.PREPARE); - this.query = query; - } - - @Override - protected Request copyInternal() { - return new Prepare(query); - } - - @Override - public String toString() { - return "PREPARE " + query; - } - } - - static class Register extends Message.Request { - - static final Message.Coder coder = - new Message.Coder() { - @Override - public void encode(Register msg, ByteBuf dest, ProtocolVersion version) { - dest.writeShort(msg.eventTypes.size()); - for (ProtocolEvent.Type type : msg.eventTypes) CBUtil.writeEnumValue(type, dest); - } - - @Override - public int encodedSize(Register msg, ProtocolVersion version) { - int size = 2; - for (ProtocolEvent.Type type : msg.eventTypes) size += CBUtil.sizeOfEnumValue(type); - return size; - } - }; - - private final List eventTypes; - - Register(List eventTypes) { - super(Message.Request.Type.REGISTER); - this.eventTypes = eventTypes; - } - - @Override - protected Request copyInternal() { - return new Register(eventTypes); - } - - @Override - public String toString() { - return "REGISTER " + eventTypes; - } - } - - static class AuthResponse extends Message.Request { - - static final Message.Coder coder = - new Message.Coder() { - - @Override - public void encode(AuthResponse response, ByteBuf dest, ProtocolVersion version) { - CBUtil.writeValue(response.token, dest); - } - - @Override - public int encodedSize(AuthResponse response, ProtocolVersion version) { - return CBUtil.sizeOfValue(response.token); - } - }; - - private final byte[] token; - - AuthResponse(byte[] token) { - super(Message.Request.Type.AUTH_RESPONSE); - this.token = token; - } - - @Override - protected Request copyInternal() { - return new AuthResponse(token); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Responses.java b/driver-core/src/main/java/com/datastax/driver/core/Responses.java deleted file mode 100644 index d160cd0332c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Responses.java +++ /dev/null @@ -1,809 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.SchemaElement.AGGREGATE; -import static com.datastax.driver.core.SchemaElement.FUNCTION; -import static com.datastax.driver.core.SchemaElement.KEYSPACE; -import static com.datastax.driver.core.SchemaElement.TABLE; - -import com.datastax.driver.core.Responses.Result.Rows.Metadata; -import com.datastax.driver.core.exceptions.AlreadyExistsException; -import com.datastax.driver.core.exceptions.AuthenticationException; -import com.datastax.driver.core.exceptions.BootstrappingException; -import com.datastax.driver.core.exceptions.CASWriteUnknownException; -import com.datastax.driver.core.exceptions.CDCWriteException; -import com.datastax.driver.core.exceptions.DriverException; -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.exceptions.FunctionExecutionException; -import com.datastax.driver.core.exceptions.InvalidConfigurationInQueryException; -import com.datastax.driver.core.exceptions.InvalidQueryException; -import com.datastax.driver.core.exceptions.OverloadedException; -import com.datastax.driver.core.exceptions.ProtocolError; -import com.datastax.driver.core.exceptions.ReadFailureException; -import com.datastax.driver.core.exceptions.ReadTimeoutException; -import com.datastax.driver.core.exceptions.ServerError; -import com.datastax.driver.core.exceptions.SyntaxError; -import com.datastax.driver.core.exceptions.TruncateException; -import com.datastax.driver.core.exceptions.UnauthorizedException; -import com.datastax.driver.core.exceptions.UnavailableException; -import com.datastax.driver.core.exceptions.UnpreparedException; -import com.datastax.driver.core.exceptions.WriteFailureException; -import com.datastax.driver.core.exceptions.WriteTimeoutException; -import com.datastax.driver.core.utils.Bytes; -import io.netty.buffer.ByteBuf; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; - -class Responses { - - private Responses() {} - - static class Error extends Message.Response { - - static final Message.Decoder decoder = - new Message.Decoder() { - @Override - public Error decode(ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - ExceptionCode code = ExceptionCode.fromValue(body.readInt()); - String msg = CBUtil.readString(body); - Object infos = null; - ConsistencyLevel clt; - int received, blockFor; - switch (code) { - case UNAVAILABLE: - ConsistencyLevel clu = CBUtil.readConsistencyLevel(body); - int required = body.readInt(); - int alive = body.readInt(); - infos = new UnavailableException(clu, required, alive); - break; - case WRITE_TIMEOUT: - case READ_TIMEOUT: - clt = CBUtil.readConsistencyLevel(body); - received = body.readInt(); - blockFor = body.readInt(); - if (code == ExceptionCode.WRITE_TIMEOUT) { - WriteType writeType = Enum.valueOf(WriteType.class, CBUtil.readString(body)); - infos = new WriteTimeoutException(clt, writeType, received, blockFor); - } else { - byte dataPresent = body.readByte(); - infos = new ReadTimeoutException(clt, received, blockFor, dataPresent != 0); - } - break; - case WRITE_FAILURE: - case READ_FAILURE: - clt = CBUtil.readConsistencyLevel(body); - received = body.readInt(); - blockFor = body.readInt(); - int failures = body.readInt(); - Map failuresMap; - if (version.compareTo(ProtocolVersion.V5) < 0) { - failuresMap = Collections.emptyMap(); - } else { - failuresMap = new HashMap(); - for (int i = 0; i < failures; i++) { - InetAddress address = CBUtil.readInetWithoutPort(body); - int reasonCode = body.readUnsignedShort(); - failuresMap.put(address, reasonCode); - } - } - if (code == ExceptionCode.WRITE_FAILURE) { - WriteType writeType = Enum.valueOf(WriteType.class, CBUtil.readString(body)); - infos = - new WriteFailureException( - clt, writeType, received, blockFor, failures, failuresMap); - } else { - byte dataPresent = body.readByte(); - infos = - new ReadFailureException( - clt, received, blockFor, failures, failuresMap, dataPresent != 0); - } - break; - case CAS_WRITE_UNKNOWN: - clt = CBUtil.readConsistencyLevel(body); - received = body.readInt(); - blockFor = body.readInt(); - infos = new CASWriteUnknownException(clt, received, blockFor); - break; - case UNPREPARED: - infos = MD5Digest.wrap(CBUtil.readBytes(body)); - break; - case ALREADY_EXISTS: - String ksName = CBUtil.readString(body); - String cfName = CBUtil.readString(body); - infos = new AlreadyExistsException(ksName, cfName); - break; - } - return new Error(version, code, msg, infos); - } - }; - - final ProtocolVersion serverProtocolVersion; - final ExceptionCode code; - final String message; - final Object infos; // can be null - - private Error( - ProtocolVersion serverProtocolVersion, ExceptionCode code, String message, Object infos) { - super(Message.Response.Type.ERROR); - this.serverProtocolVersion = serverProtocolVersion; - this.code = code; - this.message = message; - this.infos = infos; - } - - DriverException asException(EndPoint endPoint) { - switch (code) { - case SERVER_ERROR: - return new ServerError(endPoint, message); - case PROTOCOL_ERROR: - return new ProtocolError(endPoint, message); - case BAD_CREDENTIALS: - return new AuthenticationException(endPoint, message); - case UNAVAILABLE: - return ((UnavailableException) infos) - .copy(endPoint); // We copy to have a nice stack trace - case OVERLOADED: - return new OverloadedException(endPoint, message); - case IS_BOOTSTRAPPING: - return new BootstrappingException(endPoint, message); - case TRUNCATE_ERROR: - return new TruncateException(endPoint, message); - case WRITE_TIMEOUT: - return ((WriteTimeoutException) infos).copy(endPoint); - case READ_TIMEOUT: - return ((ReadTimeoutException) infos).copy(endPoint); - case WRITE_FAILURE: - return ((WriteFailureException) infos).copy(endPoint); - case READ_FAILURE: - return ((ReadFailureException) infos).copy(endPoint); - case FUNCTION_FAILURE: - return new FunctionExecutionException(endPoint, message); - case CDC_WRITE_FAILURE: - return new CDCWriteException(endPoint, message); - case CAS_WRITE_UNKNOWN: - return ((CASWriteUnknownException) infos).copy(endPoint); - case SYNTAX_ERROR: - return new SyntaxError(endPoint, message); - case UNAUTHORIZED: - return new UnauthorizedException(endPoint, message); - case INVALID: - return new InvalidQueryException(endPoint, message); - case CONFIG_ERROR: - return new InvalidConfigurationInQueryException(endPoint, message); - case ALREADY_EXISTS: - return ((AlreadyExistsException) infos).copy(endPoint); - case UNPREPARED: - return new UnpreparedException(endPoint, message); - - default: - return new DriverInternalError( - String.format( - "Unknown protocol error code %s returned by %s. The error message was: %s", - code, endPoint, message)); - } - } - - @Override - public String toString() { - return "ERROR " + code + ": " + message; - } - } - - static class Ready extends Message.Response { - - static final Message.Decoder decoder = - new Message.Decoder() { - @Override - public Ready decode(ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - // TODO: Would it be cool to return a singleton? Check we don't need to - // set the streamId or something - return new Ready(); - } - }; - - Ready() { - super(Message.Response.Type.READY); - } - - @Override - public String toString() { - return "READY"; - } - } - - static class Authenticate extends Message.Response { - - static final Message.Decoder decoder = - new Message.Decoder() { - @Override - public Authenticate decode( - ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - String authenticator = CBUtil.readString(body); - return new Authenticate(authenticator); - } - }; - - final String authenticator; - - Authenticate(String authenticator) { - super(Message.Response.Type.AUTHENTICATE); - this.authenticator = authenticator; - } - - @Override - public String toString() { - return "AUTHENTICATE " + authenticator; - } - } - - static class Supported extends Message.Response { - - static final Message.Decoder decoder = - new Message.Decoder() { - @Override - public Supported decode( - ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - return new Supported(CBUtil.readStringToStringListMap(body)); - } - }; - - final Map> supported; - final Set supportedCompressions = - EnumSet.noneOf(ProtocolOptions.Compression.class); - - Supported(Map> supported) { - super(Message.Response.Type.SUPPORTED); - this.supported = supported; - - parseCompressions(); - } - - private void parseCompressions() { - List compList = supported.get(Requests.Startup.COMPRESSION_OPTION); - if (compList == null) return; - - for (String compStr : compList) { - ProtocolOptions.Compression compr = ProtocolOptions.Compression.fromString(compStr); - if (compr != null) supportedCompressions.add(compr); - } - } - - @Override - public String toString() { - return "SUPPORTED " + supported; - } - } - - abstract static class Result extends Message.Response { - - static final Message.Decoder decoder = - new Message.Decoder() { - @Override - public Result decode(ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - Kind kind = Kind.fromId(body.readInt()); - return kind.subDecoder.decode(body, version, codecRegistry); - } - }; - - enum Kind { - VOID(1, Void.subcodec), - ROWS(2, Rows.subcodec), - SET_KEYSPACE(3, SetKeyspace.subcodec), - PREPARED(4, Prepared.subcodec), - SCHEMA_CHANGE(5, SchemaChange.subcodec); - - private final int id; - final Message.Decoder subDecoder; - - private static final Kind[] ids; - - static { - int maxId = -1; - for (Kind k : Kind.values()) maxId = Math.max(maxId, k.id); - ids = new Kind[maxId + 1]; - for (Kind k : Kind.values()) { - if (ids[k.id] != null) throw new IllegalStateException("Duplicate kind id"); - ids[k.id] = k; - } - } - - Kind(int id, Message.Decoder subDecoder) { - this.id = id; - this.subDecoder = subDecoder; - } - - static Kind fromId(int id) { - Kind k = ids[id]; - if (k == null) - throw new DriverInternalError(String.format("Unknown kind id %d in RESULT message", id)); - return k; - } - } - - final Kind kind; - - protected Result(Kind kind) { - super(Message.Response.Type.RESULT); - this.kind = kind; - } - - static class Void extends Result { - // Even though we have no specific information here, don't make a - // singleton since as each message it has in fact a streamid and connection. - Void() { - super(Kind.VOID); - } - - static final Message.Decoder subcodec = - new Message.Decoder() { - @Override - public Result decode( - ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - return new Void(); - } - }; - - @Override - public String toString() { - return "EMPTY RESULT"; - } - } - - static class SetKeyspace extends Result { - final String keyspace; - - private SetKeyspace(String keyspace) { - super(Kind.SET_KEYSPACE); - this.keyspace = keyspace; - } - - static final Message.Decoder subcodec = - new Message.Decoder() { - @Override - public Result decode( - ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - return new SetKeyspace(CBUtil.readString(body)); - } - }; - - @Override - public String toString() { - return "RESULT set keyspace " + keyspace; - } - } - - static class Rows extends Result { - - static class Metadata { - - private enum Flag { - // The order of that enum matters!! - GLOBAL_TABLES_SPEC, - HAS_MORE_PAGES, - NO_METADATA, - METADATA_CHANGED; - - static EnumSet deserialize(int flags) { - EnumSet set = EnumSet.noneOf(Flag.class); - Flag[] values = Flag.values(); - for (int n = 0; n < values.length; n++) { - if ((flags & (1 << n)) != 0) set.add(values[n]); - } - return set; - } - - static int serialize(EnumSet flags) { - int i = 0; - for (Flag flag : flags) i |= 1 << flag.ordinal(); - return i; - } - } - - static final Metadata EMPTY = new Metadata(null, 0, null, null, null); - - final int columnCount; - final ColumnDefinitions columns; // Can be null if no metadata was asked by the query - final ByteBuffer pagingState; - final int[] pkIndices; - final MD5Digest - metadataId; // only present if the flag METADATA_CHANGED is set (ROWS response only) - - private Metadata( - MD5Digest metadataId, - int columnCount, - ColumnDefinitions columns, - ByteBuffer pagingState, - int[] pkIndices) { - this.metadataId = metadataId; - this.columnCount = columnCount; - this.columns = columns; - this.pagingState = pagingState; - this.pkIndices = pkIndices; - } - - static Metadata decode( - ByteBuf body, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - return decode(body, false, protocolVersion, codecRegistry); - } - - static Metadata decode( - ByteBuf body, - boolean withPkIndices, - ProtocolVersion protocolVersion, - CodecRegistry codecRegistry) { - - // flags & column count - EnumSet flags = Flag.deserialize(body.readInt()); - int columnCount = body.readInt(); - - ByteBuffer state = null; - if (flags.contains(Flag.HAS_MORE_PAGES)) state = CBUtil.readValue(body); - - MD5Digest resultMetadataId = null; - if (flags.contains(Flag.METADATA_CHANGED)) { - assert ProtocolFeature.PREPARED_METADATA_CHANGES.isSupportedBy(protocolVersion) - : "METADATA_CHANGED flag is not supported in protocol version " + protocolVersion; - assert !flags.contains(Flag.NO_METADATA) - : "METADATA_CHANGED and NO_METADATA are mutually exclusive flags"; - resultMetadataId = MD5Digest.wrap(CBUtil.readBytes(body)); - } - - int[] pkIndices = null; - int pkCount; - if (withPkIndices && (pkCount = body.readInt()) > 0) { - pkIndices = new int[pkCount]; - for (int i = 0; i < pkCount; i++) pkIndices[i] = (int) body.readShort(); - } - - if (flags.contains(Flag.NO_METADATA)) - return new Metadata(resultMetadataId, columnCount, null, state, pkIndices); - - boolean globalTablesSpec = flags.contains(Flag.GLOBAL_TABLES_SPEC); - - String globalKsName = null; - String globalCfName = null; - if (globalTablesSpec) { - globalKsName = CBUtil.readString(body); - globalCfName = CBUtil.readString(body); - } - - // metadata (names/types) - ColumnDefinitions.Definition[] defs = new ColumnDefinitions.Definition[columnCount]; - for (int i = 0; i < columnCount; i++) { - String ksName = globalTablesSpec ? globalKsName : CBUtil.readString(body); - String cfName = globalTablesSpec ? globalCfName : CBUtil.readString(body); - String name = CBUtil.readString(body); - DataType type = DataType.decode(body, protocolVersion, codecRegistry); - defs[i] = new ColumnDefinitions.Definition(ksName, cfName, name, type); - } - - return new Metadata( - resultMetadataId, - columnCount, - new ColumnDefinitions(defs, codecRegistry), - state, - pkIndices); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - - if (columns == null) { - sb.append('[').append(columnCount).append(" columns]"); - } else { - for (ColumnDefinitions.Definition column : columns) { - sb.append('[').append(column.getName()); - sb.append(" (").append(column.getType()).append(")]"); - } - } - if (pagingState != null) sb.append(" (to be continued)"); - return sb.toString(); - } - } - - static final Message.Decoder subcodec = - new Message.Decoder() { - @Override - public Result decode( - ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - - Metadata metadata = Metadata.decode(body, version, codecRegistry); - - int rowCount = body.readInt(); - int columnCount = metadata.columnCount; - - Queue> data = new ArrayDeque>(rowCount); - for (int i = 0; i < rowCount; i++) { - List row = new ArrayList(columnCount); - for (int j = 0; j < columnCount; j++) row.add(CBUtil.readValue(body)); - data.add(row); - } - - return new Rows(metadata, data, version); - } - }; - - final Metadata metadata; - final Queue> data; - private final ProtocolVersion version; - - private Rows(Metadata metadata, Queue> data, ProtocolVersion version) { - super(Kind.ROWS); - this.metadata = metadata; - this.data = data; - this.version = version; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("ROWS ").append(metadata).append('\n'); - for (List row : data) { - for (int i = 0; i < row.size(); i++) { - ByteBuffer v = row.get(i); - if (v == null) { - sb.append(" | null"); - } else { - sb.append(" | "); - if (metadata.columns != null) { - DataType dataType = metadata.columns.getType(i); - sb.append(dataType); - sb.append(" "); - TypeCodec codec = metadata.columns.codecRegistry.codecFor(dataType); - Object o = codec.deserialize(v, version); - String s = codec.format(o); - if (s.length() > 100) s = s.substring(0, 100) + "..."; - sb.append(s); - } else { - sb.append(Bytes.toHexString(v)); - } - } - } - sb.append('\n'); - } - sb.append("---"); - return sb.toString(); - } - } - - static class Prepared extends Result { - - static final Message.Decoder subcodec = - new Message.Decoder() { - @Override - public Result decode( - ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - MD5Digest id = MD5Digest.wrap(CBUtil.readBytes(body)); - MD5Digest resultMetadataId = null; - if (ProtocolFeature.PREPARED_METADATA_CHANGES.isSupportedBy(version)) - resultMetadataId = MD5Digest.wrap(CBUtil.readBytes(body)); - boolean withPkIndices = version.compareTo(ProtocolVersion.V4) >= 0; - Rows.Metadata metadata = - Rows.Metadata.decode(body, withPkIndices, version, codecRegistry); - Rows.Metadata resultMetadata = decodeResultMetadata(body, version, codecRegistry); - return new Prepared(id, resultMetadataId, metadata, resultMetadata); - } - - private Metadata decodeResultMetadata( - ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - switch (version) { - case V1: - return Rows.Metadata.EMPTY; - case V2: - case V3: - case V4: - case V5: - case V6: - return Rows.Metadata.decode(body, version, codecRegistry); - default: - throw version.unsupported(); - } - } - }; - - final MD5Digest statementId; - final MD5Digest resultMetadataId; - final Rows.Metadata metadata; - final Rows.Metadata resultMetadata; - - private Prepared( - MD5Digest statementId, - MD5Digest resultMetadataId, - Rows.Metadata metadata, - Rows.Metadata resultMetadata) { - super(Kind.PREPARED); - this.statementId = statementId; - this.resultMetadataId = resultMetadataId; - this.metadata = metadata; - this.resultMetadata = resultMetadata; - } - - @Override - public String toString() { - return "RESULT PREPARED " - + statementId - + ' ' - + metadata - + " (resultMetadata=" - + resultMetadata - + ')'; - } - } - - static class SchemaChange extends Result { - - enum Change { - CREATED, - UPDATED, - DROPPED - } - - final Change change; - final SchemaElement targetType; - final String targetKeyspace; - final String targetName; - final List targetSignature; - - static final Message.Decoder subcodec = - new Message.Decoder() { - @Override - public Result decode( - ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - // Note: the CREATE KEYSPACE/TABLE/TYPE SCHEMA_CHANGE response is different from the - // SCHEMA_CHANGE EVENT type - Change change; - SchemaElement targetType; - String targetKeyspace, targetName; - List targetSignature; - switch (version) { - case V1: - case V2: - change = CBUtil.readEnumValue(Change.class, body); - targetKeyspace = CBUtil.readString(body); - targetName = CBUtil.readString(body); - targetType = targetName.isEmpty() ? KEYSPACE : TABLE; - targetSignature = Collections.emptyList(); - return new SchemaChange( - change, targetType, targetKeyspace, targetName, targetSignature); - case V3: - case V4: - case V5: - case V6: - change = CBUtil.readEnumValue(Change.class, body); - targetType = CBUtil.readEnumValue(SchemaElement.class, body); - targetKeyspace = CBUtil.readString(body); - targetName = (targetType == KEYSPACE) ? "" : CBUtil.readString(body); - targetSignature = - (targetType == FUNCTION || targetType == AGGREGATE) - ? CBUtil.readStringList(body) - : Collections.emptyList(); - return new SchemaChange( - change, targetType, targetKeyspace, targetName, targetSignature); - default: - throw version.unsupported(); - } - } - }; - - private SchemaChange( - Change change, - SchemaElement targetType, - String targetKeyspace, - String targetName, - List targetSignature) { - super(Kind.SCHEMA_CHANGE); - this.change = change; - this.targetType = targetType; - this.targetKeyspace = targetKeyspace; - this.targetName = targetName; - this.targetSignature = targetSignature; - } - - @Override - public String toString() { - return "RESULT schema change " - + change - + " on " - + targetType - + ' ' - + targetKeyspace - + (targetName.isEmpty() ? "" : '.' + targetName); - } - } - } - - static class Event extends Message.Response { - - static final Message.Decoder decoder = - new Message.Decoder() { - @Override - public Event decode(ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - return new Event(ProtocolEvent.deserialize(body, version)); - } - }; - - final ProtocolEvent event; - - Event(ProtocolEvent event) { - super(Message.Response.Type.EVENT); - this.event = event; - } - - @Override - public String toString() { - return "EVENT " + event; - } - } - - static class AuthChallenge extends Message.Response { - - static final Message.Decoder decoder = - new Message.Decoder() { - @Override - public AuthChallenge decode( - ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - ByteBuffer b = CBUtil.readValue(body); - if (b == null) return new AuthChallenge(null); - - byte[] token = new byte[b.remaining()]; - b.get(token); - return new AuthChallenge(token); - } - }; - - final byte[] token; - - private AuthChallenge(byte[] token) { - super(Message.Response.Type.AUTH_CHALLENGE); - this.token = token; - } - } - - static class AuthSuccess extends Message.Response { - - static final Message.Decoder decoder = - new Message.Decoder() { - @Override - public AuthSuccess decode( - ByteBuf body, ProtocolVersion version, CodecRegistry codecRegistry) { - ByteBuffer b = CBUtil.readValue(body); - if (b == null) return new AuthSuccess(null); - - byte[] token = new byte[b.remaining()]; - b.get(token); - return new AuthSuccess(token); - } - }; - - final byte[] token; - - private AuthSuccess(byte[] token) { - super(Message.Response.Type.AUTH_SUCCESS); - this.token = token; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ResultSet.java b/driver-core/src/main/java/com/datastax/driver/core/ResultSet.java deleted file mode 100644 index 4b9902d32ce..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ResultSet.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * The result of a query. - * - *

The retrieval of the rows of a ResultSet is generally paged (a first page of result is fetched - * and the next one is only fetched once all the results of the first one has been consumed). The - * size of the pages can be configured either globally through {@link QueryOptions#setFetchSize} or - * per-statement with {@link Statement#setFetchSize}. Though new pages are automatically (and - * transparently) fetched when needed, it is possible to force the retrieval of the next page early - * through {@link #fetchMoreResults}. Please note however that this ResultSet paging is not - * available with the version 1 of the native protocol (i.e. with Cassandra 1.2 or if version 1 has - * been explicitly requested through {@link Cluster.Builder#withProtocolVersion}). If the protocol - * version 1 is in use, a ResultSet is always fetched in it's entirely and it's up to the client to - * make sure that no query can yield ResultSet that won't hold in memory. - * - *

Note that this class is not thread-safe. - */ -public interface ResultSet extends PagingIterable { - - // redeclared only to make clirr happy - @Override - Row one(); - - /** - * Returns the columns returned in this ResultSet. - * - * @return the columns returned in this ResultSet. - */ - public ColumnDefinitions getColumnDefinitions(); - - /** - * If the query that produced this ResultSet was a conditional update, return whether it was - * successfully applied. - * - *

This is equivalent to calling: - * - *

- * - *

-   * rs.one().getBool("[applied]");
-   * 
- * - * Except that this method peeks at the next row without consuming it. - * - *

For consistency, this method always returns {@code true} for non-conditional queries - * (although there is no reason to call the method in that case). This is also the case for - * conditional DDL statements ({@code CREATE KEYSPACE... IF NOT EXISTS}, {@code CREATE TABLE... IF - * NOT EXISTS}), for which Cassandra doesn't return an {@code [applied]} column. - * - *

Note that, for versions of Cassandra strictly lower than 2.0.9 and 2.1.0-rc2, a server-side - * bug (CASSANDRA-7337) causes this method to always return {@code true} for batches containing - * conditional queries. - * - * @return if the query was a conditional update, whether it was applied. {@code true} for other - * types of queries. - * @see CASSANDRA-7337 - */ - public boolean wasApplied(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ResultSetFuture.java b/driver-core/src/main/java/com/datastax/driver/core/ResultSetFuture.java deleted file mode 100644 index c78c4e08183..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ResultSetFuture.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.NoHostAvailableException; -import com.datastax.driver.core.exceptions.QueryExecutionException; -import com.datastax.driver.core.exceptions.QueryValidationException; -import com.google.common.util.concurrent.ListenableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -/** - * A future on a {@link ResultSet}. - * - *

Note that this class implements Guava's {@code - * ListenableFuture} and can so be used with Guava's future utilities. - */ -public interface ResultSetFuture extends ListenableFuture { - - /** - * Waits for the query to return and return its result. - * - *

This method is usually more convenient than {@link #get} because it: - * - *

    - *
  • Waits for the result uninterruptibly, and so doesn't throw {@link InterruptedException}. - *
  • Returns meaningful exceptions, instead of having to deal with ExecutionException. - *
- * - * As such, it is the preferred way to get the future result. - * - * @return the query result set. - * @throws NoHostAvailableException if no host in the cluster can be contacted successfully to - * execute this query. - * @throws QueryExecutionException if the query triggered an execution exception, that is an - * exception thrown by Cassandra when it cannot execute the query with the requested - * consistency level successfully. - * @throws QueryValidationException if the query is invalid (syntax error, unauthorized or any - * other validation problem). - */ - public ResultSet getUninterruptibly(); - - /** - * Waits for the provided time for the query to return and return its result if available. - * - *

This method is usually more convenient than {@link #get} because it: - * - *

    - *
  • Waits for the result uninterruptibly, and so doesn't throw {@link InterruptedException}. - *
  • Returns meaningful exceptions, instead of having to deal with ExecutionException. - *
- * - * As such, it is the preferred way to get the future result. - * - * @param timeout the time to wait for the query to return. - * @param unit the unit for {@code timeout}. - * @return the query result set. - * @throws NoHostAvailableException if no host in the cluster can be contacted successfully to - * execute this query. - * @throws QueryExecutionException if the query triggered an execution exception, that is an - * exception thrown by Cassandra when it cannot execute the query with the requested - * consistency level successfully. - * @throws QueryValidationException if the query if invalid (syntax error, unauthorized or any - * other validation problem). - * @throws TimeoutException if the wait timed out (Note that this is different from a Cassandra - * timeout, which is a {@code QueryExecutionException}). - */ - public ResultSet getUninterruptibly(long timeout, TimeUnit unit) throws TimeoutException; - - /** - * Attempts to cancel the execution of the request corresponding to this future. This attempt will - * fail if the request has already returned. - * - *

Please note that this only cancel the request driver side, but nothing is done to interrupt - * the execution of the request Cassandra side (and that even if {@code mayInterruptIfRunning} is - * true) since Cassandra does not support such interruption. - * - *

This method can be used to ensure no more work is performed driver side (which, while it - * doesn't include stopping a request already submitted to a Cassandra node, may include not - * retrying another Cassandra host on failure/timeout) if the ResultSet is not going to be - * retried. Typically, the code to wait for a request result for a maximum of 1 second could look - * like: - * - *

-   *   ResultSetFuture future = session.executeAsync(...some query...);
-   *   try {
-   *       ResultSet result = future.get(1, TimeUnit.SECONDS);
-   *       ... process result ...
-   *   } catch (TimeoutException e) {
-   *       future.cancel(true); // Ensure any resource used by this query driver
-   *                            // side is released immediately
-   *       ... handle timeout ...
-   *   }
-   * 
- * - * @param mayInterruptIfRunning the value of this parameter is currently ignored. - * @return {@code false} if the future could not be cancelled (it has already completed normally); - * {@code true} otherwise. - */ - @Override - public boolean cancel(boolean mayInterruptIfRunning); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Row.java b/driver-core/src/main/java/com/datastax/driver/core/Row.java deleted file mode 100644 index 879086f83f9..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Row.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.InvalidTypeException; - -/** - * A CQL Row returned in a {@link ResultSet}. - * - *

The values of a CQL Row can be retrieved by either index (index starts at zero) or name. When - * getting them by name, names follow the case insensitivity rules explained in {@link - * ColumnDefinitions}. - */ -public interface Row extends GettableData { - - /** - * Returns the columns contained in this Row. - * - * @return the columns contained in this Row. - */ - public ColumnDefinitions getColumnDefinitions(); - - /** - * Returns the {@code i}th value of this row as a {@link Token}. - * - *

{@link #getPartitionKeyToken()} should generally be preferred to this method (unless the - * token column is aliased). - * - * @param i the index ({@code 0 <= i < size()}) of the column to retrieve. - * @return the value of the {@code i}th column in this row as an Token. - * @throws IndexOutOfBoundsException if {@code i < 0 || i >= this.columns().size()}. - * @throws InvalidTypeException if column {@code i} is not of the type of token values for this - * cluster (this depends on the configured partitioner). - */ - public Token getToken(int i); - - /** - * Returns the value of column {@code name} as a {@link Token}. - * - *

{@link #getPartitionKeyToken()} should generally be preferred to this method (unless the - * token column is aliased). - * - * @param name the name of the column to retrieve. - * @return the value of column {@code name} as a Token. - * @throws IllegalArgumentException if {@code name} is not part of the ResultSet this row is part - * of, i.e. if {@code !this.columns().names().contains(name)}. - * @throws InvalidTypeException if column {@code name} is not of the type of token values for this - * cluster (this depends on the configured partitioner). - */ - public Token getToken(String name); - - /** - * Returns the value of the first column containing a {@link Token}. - * - *

This method is a shorthand for queries returning a single token in an unaliased column. It - * will look for the first name matching {@code token(...)}: - * - *

{@code
-   * ResultSet rs = session.execute("SELECT token(k) FROM my_table WHERE k = 1");
-   * Token token = rs.one().getPartitionKeyToken(); // retrieves token(k)
-   * }
- * - * If that doesn't work for you (for example, if you're using an alias), use {@link - * #getToken(int)} or {@link #getToken(String)}. - * - * @return the value of column {@code name} as a Token. - * @throws IllegalStateException if no column named {@code token(...)} exists in this ResultSet. - * @throws InvalidTypeException if the first column named {@code token(...)} is not of the type of - * token values for this cluster (this depends on the configured partitioner). - */ - public Token getPartitionKeyToken(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SSLOptions.java b/driver-core/src/main/java/com/datastax/driver/core/SSLOptions.java deleted file mode 100644 index 94e003280d1..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SSLOptions.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.ssl.SslHandler; - -/** - * Defines how the driver configures SSL connections. - * - *

Note: since version 3.2.0, users are encouraged to implement {@link - * RemoteEndpointAwareSSLOptions} instead. - * - * @see RemoteEndpointAwareSSLOptions - * @see JdkSSLOptions - * @see NettySSLOptions - */ -@SuppressWarnings("deprecation") -public interface SSLOptions { - - /** - * Creates a new SSL handler for the given Netty channel. - * - *

This gets called each time the driver opens a new connection to a Cassandra host. The newly - * created handler will be added to the channel's pipeline to provide SSL support for the - * connection. - * - *

You don't necessarily need to implement this method directly; see the provided - * implementations: {@link JdkSSLOptions} and {@link NettySSLOptions}. - * - * @param channel the channel. - * @return the handler. - * @deprecated use {@link RemoteEndpointAwareSSLOptions#newSSLHandler(SocketChannel, EndPoint)} - * instead. - */ - @SuppressWarnings("DeprecatedIsStillUsed") - @Deprecated - SslHandler newSSLHandler(SocketChannel channel); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SchemaChangeListener.java b/driver-core/src/main/java/com/datastax/driver/core/SchemaChangeListener.java deleted file mode 100644 index d20d0fbd755..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SchemaChangeListener.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Interface for objects that are interested in tracking schema change events in the cluster. - * - *

An implementation of this interface can be registered against a Cluster object through the - * {@link com.datastax.driver.core.Cluster#register(SchemaChangeListener)} method. - * - *

Note that the methods defined by this interface will be executed by internal driver threads, - * and are therefore expected to have short execution times. If you need to perform long - * computations or blocking calls in response to schema change events, it is strongly recommended to - * schedule them asynchronously on a separate thread provided by your application code. - */ -public interface SchemaChangeListener { - - /** - * Called when a keyspace has been added. - * - * @param keyspace the keyspace that has been added. - */ - void onKeyspaceAdded(KeyspaceMetadata keyspace); - - /** - * Called when a keyspace has been removed. - * - * @param keyspace the keyspace that has been removed. - */ - void onKeyspaceRemoved(KeyspaceMetadata keyspace); - - /** - * Called when a keyspace has changed. - * - * @param current the keyspace that has changed, in its current form (after the change). - * @param previous the keyspace that has changed, in its previous form (before the change). - */ - void onKeyspaceChanged(KeyspaceMetadata current, KeyspaceMetadata previous); - - /** - * Called when a table has been added. - * - * @param table the table that has been newly added. - */ - void onTableAdded(TableMetadata table); - - /** - * Called when a table has been removed. - * - * @param table the table that has been removed. - */ - void onTableRemoved(TableMetadata table); - - /** - * Called when a table has changed. - * - * @param current the table that has changed, in its current form (after the change). - * @param previous the table that has changed, in its previous form (before the change). - */ - void onTableChanged(TableMetadata current, TableMetadata previous); - - /** - * Called when a user-defined type has been added. - * - * @param type the type that has been newly added. - */ - void onUserTypeAdded(UserType type); - - /** - * Called when a user-defined type has been removed. - * - * @param type the type that has been removed. - */ - void onUserTypeRemoved(UserType type); - - /** - * Called when a user-defined type has changed. - * - * @param current the type that has changed, in its current form (after the change). - * @param previous the type that has changed, in its previous form (before the change). - */ - void onUserTypeChanged(UserType current, UserType previous); - - /** - * Called when a user-defined function has been added. - * - * @param function the function that has been newly added. - */ - void onFunctionAdded(FunctionMetadata function); - - /** - * Called when a user-defined function has been removed. - * - * @param function the function that has been removed. - */ - void onFunctionRemoved(FunctionMetadata function); - - /** - * Called when a user-defined function has changed. - * - * @param current the function that has changed, in its current form (after the change). - * @param previous the function that has changed, in its previous form (before the change). - */ - void onFunctionChanged(FunctionMetadata current, FunctionMetadata previous); - - /** - * Called when a user-defined aggregate has been added. - * - * @param aggregate the aggregate that has been newly added. - */ - void onAggregateAdded(AggregateMetadata aggregate); - - /** - * Called when a user-defined aggregate has been removed. - * - * @param aggregate the aggregate that has been removed. - */ - void onAggregateRemoved(AggregateMetadata aggregate); - - /** - * Called when a user-defined aggregate has changed. - * - * @param current the aggregate that has changed, in its current form (after the change). - * @param previous the aggregate that has changed, in its previous form (before the change). - */ - void onAggregateChanged(AggregateMetadata current, AggregateMetadata previous); - - /** - * Called when a materialized view has been added. - * - * @param view the materialized view that has been newly added. - */ - void onMaterializedViewAdded(MaterializedViewMetadata view); - - /** - * Called when a materialized view has been removed. - * - * @param view the materialized view that has been removed. - */ - void onMaterializedViewRemoved(MaterializedViewMetadata view); - - /** - * Called when a materialized view has changed. - * - * @param current the materialized view that has changed, in its current form (after the change). - * @param previous the materialized view that has changed, in its previous form (before the - * change). - */ - void onMaterializedViewChanged( - MaterializedViewMetadata current, MaterializedViewMetadata previous); - - /** - * Gets invoked when the listener is registered with a cluster. - * - * @param cluster the cluster that this tracker is registered with. - */ - void onRegister(Cluster cluster); - - /** - * Gets invoked when the listener is unregistered from a cluster, or at cluster shutdown if the - * tracker was not unregistered. - * - * @param cluster the cluster that this tracker was registered with. - */ - void onUnregister(Cluster cluster); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SchemaChangeListenerBase.java b/driver-core/src/main/java/com/datastax/driver/core/SchemaChangeListenerBase.java deleted file mode 100644 index b2185633174..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SchemaChangeListenerBase.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** Base implementation for {@link SchemaChangeListener}. */ -public abstract class SchemaChangeListenerBase implements SchemaChangeListener { - - @Override - public void onKeyspaceAdded(KeyspaceMetadata keyspace) {} - - @Override - public void onKeyspaceRemoved(KeyspaceMetadata keyspace) {} - - @Override - public void onKeyspaceChanged(KeyspaceMetadata current, KeyspaceMetadata previous) {} - - @Override - public void onTableAdded(TableMetadata table) {} - - @Override - public void onTableRemoved(TableMetadata table) {} - - @Override - public void onTableChanged(TableMetadata current, TableMetadata previous) {} - - @Override - public void onUserTypeAdded(UserType type) {} - - @Override - public void onUserTypeRemoved(UserType type) {} - - @Override - public void onUserTypeChanged(UserType current, UserType previous) {} - - @Override - public void onFunctionAdded(FunctionMetadata function) {} - - @Override - public void onFunctionRemoved(FunctionMetadata function) {} - - @Override - public void onFunctionChanged(FunctionMetadata current, FunctionMetadata previous) {} - - @Override - public void onAggregateAdded(AggregateMetadata aggregate) {} - - @Override - public void onAggregateRemoved(AggregateMetadata aggregate) {} - - @Override - public void onAggregateChanged(AggregateMetadata current, AggregateMetadata previous) {} - - @Override - public void onMaterializedViewAdded(MaterializedViewMetadata view) {} - - @Override - public void onMaterializedViewRemoved(MaterializedViewMetadata view) {} - - @Override - public void onMaterializedViewChanged( - MaterializedViewMetadata current, MaterializedViewMetadata previous) {} - - @Override - public void onRegister(Cluster cluster) {} - - @Override - public void onUnregister(Cluster cluster) {} -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SchemaElement.java b/driver-core/src/main/java/com/datastax/driver/core/SchemaElement.java deleted file mode 100644 index 36d1e1d2a00..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SchemaElement.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Values for a SCHEMA_CHANGE event. See protocol v4 section 4.2.6. Note that {@code VIEW} is not a - * valid string under protocol v4 or lower, but is included for internal use only. - */ -enum SchemaElement { - KEYSPACE, - TABLE, - TYPE, - FUNCTION, - AGGREGATE, - VIEW -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SchemaParser.java b/driver-core/src/main/java/com/datastax/driver/core/SchemaParser.java deleted file mode 100644 index 74443a22741..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SchemaParser.java +++ /dev/null @@ -1,1188 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.SchemaElement.AGGREGATE; -import static com.datastax.driver.core.SchemaElement.FUNCTION; -import static com.datastax.driver.core.SchemaElement.KEYSPACE; -import static com.datastax.driver.core.SchemaElement.TABLE; -import static com.datastax.driver.core.SchemaElement.TYPE; -import static com.datastax.driver.core.SchemaElement.VIEW; - -import com.datastax.driver.core.exceptions.BusyConnectionException; -import com.datastax.driver.core.exceptions.ConnectionException; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -abstract class SchemaParser { - - private static final Logger logger = LoggerFactory.getLogger(SchemaParser.class); - - private static final TypeCodec> LIST_OF_TEXT_CODEC = - TypeCodec.list(TypeCodec.varchar()); - - private static final SchemaParser V2_PARSER = new V2SchemaParser(); - private static final SchemaParser V3_PARSER = new V3SchemaParser(); - private static final SchemaParser V4_PARSER = new V4SchemaParser(); - - static SchemaParser forVersion(VersionNumber cassandraVersion) { - if (cassandraVersion.getMajor() >= 4) return V4_PARSER; - if (cassandraVersion.getMajor() >= 3) return V3_PARSER; - return V2_PARSER; - } - - static SchemaParser forDseVersion(VersionNumber dseVersion) { - if (dseVersion.getMajor() == 6 && dseVersion.getMinor() >= 8) return V4_PARSER; - if (dseVersion.getMajor() >= 5) return V3_PARSER; - return V2_PARSER; - } - - abstract SystemRows fetchSystemRows( - Cluster cluster, - SchemaElement targetType, - String targetKeyspace, - String targetName, - List targetSignature, - Connection connection, - VersionNumber cassandraVersion) - throws ConnectionException, BusyConnectionException, ExecutionException, InterruptedException; - - abstract String tableNameColumn(); - - void refresh( - Cluster cluster, - SchemaElement targetType, - String targetKeyspace, - String targetName, - List targetSignature, - Connection connection, - VersionNumber cassandraVersion) - throws ConnectionException, BusyConnectionException, ExecutionException, - InterruptedException { - - SystemRows rows = - fetchSystemRows( - cluster, - targetType, - targetKeyspace, - targetName, - targetSignature, - connection, - cassandraVersion); - - Metadata metadata; - try { - metadata = cluster.getMetadata(); - } catch (IllegalStateException e) { - logger.warn("Unable to refresh metadata, cluster has been closed"); - return; - } - metadata.lock.lock(); - try { - if (targetType == null || targetType == KEYSPACE) { - // building the whole schema or a keyspace - assert rows.keyspaces != null; - Map keyspaces = buildKeyspaces(rows, cassandraVersion, cluster); - updateKeyspaces(metadata, metadata.keyspaces, keyspaces, targetKeyspace); - // If we rebuild all from scratch or have an updated keyspace, rebuild the token map - // since some replication on some keyspace may have changed - metadata.rebuildTokenMap(); - } else { - assert targetKeyspace != null; - KeyspaceMetadata keyspace = metadata.keyspaces.get(targetKeyspace); - // If we update a keyspace we don't know about, something went - // wrong. Log an error and schedule a full schema rebuild. - if (keyspace == null) { - logger.info( - String.format( - "Asked to rebuild %s %s.%s but I don't know keyspace %s", - targetType, targetKeyspace, targetName, targetKeyspace)); - metadata.cluster.submitSchemaRefresh(null, null, null, null); - } else { - switch (targetType) { - case TABLE: - if (rows.tables.containsKey(targetKeyspace)) { - Map tables = - buildTables( - keyspace, - rows.tables.get(targetKeyspace), - rows.columns.get(targetKeyspace), - rows.indexes.get(targetKeyspace), - cassandraVersion, - cluster); - updateTables(metadata, keyspace.tables, tables, targetName); - } - if (rows.views.containsKey(targetKeyspace)) { - Map tables = - buildViews( - keyspace, - rows.views.get(targetKeyspace), - rows.columns.get(targetKeyspace), - cassandraVersion, - cluster); - updateViews(metadata, keyspace.views, tables, targetName); - } - break; - case TYPE: - if (rows.udts.containsKey(targetKeyspace)) { - Map userTypes = - buildUserTypes( - keyspace, rows.udts.get(targetKeyspace), cassandraVersion, cluster); - updateUserTypes(metadata, keyspace.userTypes, userTypes, targetName); - } - break; - case FUNCTION: - if (rows.functions.containsKey(targetKeyspace)) { - Map functions = - buildFunctions( - keyspace, rows.functions.get(targetKeyspace), cassandraVersion, cluster); - updateFunctions(metadata, keyspace.functions, functions, targetName); - } - break; - case AGGREGATE: - if (rows.aggregates.containsKey(targetKeyspace)) { - Map aggregates = - buildAggregates( - keyspace, rows.aggregates.get(targetKeyspace), cassandraVersion, cluster); - updateAggregates(metadata, keyspace.aggregates, aggregates, targetName); - } - break; - } - } - } - } catch (RuntimeException e) { - // Failure to parse the schema is definitively wrong so log a full-on error, but this won't - // generally prevent queries to - // work and this can happen when new Cassandra versions modify stuff in the schema and the - // driver hasn't yet be modified. - // So log, but let things go otherwise. - logger.error( - "Error parsing schema from Cassandra system tables: the schema in Cluster#getMetadata() will appear incomplete or stale", - e); - } finally { - metadata.lock.unlock(); - } - } - - private Map buildKeyspaces( - SystemRows rows, VersionNumber cassandraVersion, Cluster cluster) { - - Map keyspaces = new LinkedHashMap(); - for (Row keyspaceRow : rows.keyspaces) { - KeyspaceMetadata keyspace = KeyspaceMetadata.build(keyspaceRow, cassandraVersion); - Map userTypes = - buildUserTypes(keyspace, rows.udts.get(keyspace.getName()), cassandraVersion, cluster); - for (UserType userType : userTypes.values()) { - keyspace.add(userType); - } - Map tables = - buildTables( - keyspace, - rows.tables.get(keyspace.getName()), - rows.columns.get(keyspace.getName()), - rows.indexes.get(keyspace.getName()), - cassandraVersion, - cluster); - for (TableMetadata table : tables.values()) { - keyspace.add(table); - } - Map functions = - buildFunctions( - keyspace, rows.functions.get(keyspace.getName()), cassandraVersion, cluster); - for (FunctionMetadata function : functions.values()) { - keyspace.add(function); - } - Map aggregates = - buildAggregates( - keyspace, rows.aggregates.get(keyspace.getName()), cassandraVersion, cluster); - for (AggregateMetadata aggregate : aggregates.values()) { - keyspace.add(aggregate); - } - Map views = - buildViews( - keyspace, - rows.views.get(keyspace.getName()), - rows.columns.get(keyspace.getName()), - cassandraVersion, - cluster); - for (MaterializedViewMetadata view : views.values()) { - keyspace.add(view); - } - keyspaces.put(keyspace.getName(), keyspace); - } - if (rows.virtualKeyspaces != null) { - for (Row keyspaceRow : rows.virtualKeyspaces) { - KeyspaceMetadata keyspace = KeyspaceMetadata.buildVirtual(keyspaceRow, cassandraVersion); - Map tables = - buildTables( - keyspace, - rows.virtualTables.get(keyspace.getName()), - rows.virtualColumns.get(keyspace.getName()), - Collections.>emptyMap(), - cassandraVersion, - cluster); - for (TableMetadata table : tables.values()) { - keyspace.add(table); - } - keyspaces.put(keyspace.getName(), keyspace); - } - } - - return keyspaces; - } - - private Map buildTables( - KeyspaceMetadata keyspace, - List tableRows, - Map> colsDefs, - Map> indexDefs, - VersionNumber cassandraVersion, - Cluster cluster) { - Map tables = new LinkedHashMap(); - if (tableRows != null) { - for (Row tableDef : tableRows) { - String cfName = tableDef.getString(tableNameColumn()); - try { - Map cols = colsDefs == null ? null : colsDefs.get(cfName); - if (cols == null || cols.isEmpty()) { - if (cassandraVersion.getMajor() >= 2) { - // In C* >= 2.0, we should never have no columns metadata because at the very least we - // should - // have the metadata corresponding to the default CQL metadata. So if we don't have - // any columns, - // that can only mean that the table got creating concurrently with our schema - // queries, and the - // query for columns metadata reached the node before the table was persisted while - // the table - // metadata one reached it afterwards. We could make the query to the column metadata - // sequential - // with the table metadata instead of in parallel, but it's probably not worth making - // it slower - // all the time to avoid this race since 1) it's very very uncommon and 2) we can just - // ignore the - // incomplete table here for now and it'll get updated next time with no particular - // consequence - // (if the table creation was concurrent with our querying, we'll get a notifciation - // later and - // will reupdate the schema for it anyway). See JAVA-320 for why we need this. - continue; - } else { - // C* 1.2 don't persists default CQL metadata, so it's possible not to have columns - // (for thirft - // tables). But in that case TableMetadata.build() knows how to handle it. - cols = Collections.emptyMap(); - } - } - List cfIndexes = (indexDefs == null) ? null : indexDefs.get(cfName); - TableMetadata table = - TableMetadata.build( - keyspace, - tableDef, - cols, - cfIndexes, - tableNameColumn(), - cassandraVersion, - cluster); - tables.put(table.getName(), table); - } catch (RuntimeException e) { - // See #refresh for why we'd rather not propagate this further - logger.error( - String.format( - "Error parsing schema for table %s.%s: " - + "Cluster.getMetadata().getKeyspace(\"%s\").getTable(\"%s\") will be missing or incomplete", - keyspace.getName(), cfName, keyspace.getName(), cfName), - e); - } - } - } - return tables; - } - - private Map buildUserTypes( - KeyspaceMetadata keyspace, - List udtRows, - VersionNumber cassandraVersion, - Cluster cluster) { - Map userTypes = new LinkedHashMap(); - if (udtRows != null) { - for (Row udtRow : maybeSortUdts(udtRows, cluster, keyspace.getName())) { - UserType type = UserType.build(keyspace, udtRow, cassandraVersion, cluster, userTypes); - userTypes.put(type.getTypeName(), type); - } - } - return userTypes; - } - - // Some schema versions require parsing UDTs in a specific order - protected List maybeSortUdts(List udtRows, Cluster cluster, String keyspace) { - return udtRows; - } - - private Map buildFunctions( - KeyspaceMetadata keyspace, - List functionRows, - VersionNumber cassandraVersion, - Cluster cluster) { - Map functions = new LinkedHashMap(); - if (functionRows != null) { - for (Row functionRow : functionRows) { - FunctionMetadata function = - FunctionMetadata.build(keyspace, functionRow, cassandraVersion, cluster); - if (function != null) { - String name = - Metadata.fullFunctionName(function.getSimpleName(), function.getArguments().values()); - functions.put(name, function); - } - } - } - return functions; - } - - private Map buildAggregates( - KeyspaceMetadata keyspace, - List aggregateRows, - VersionNumber cassandraVersion, - Cluster cluster) { - Map aggregates = new LinkedHashMap(); - if (aggregateRows != null) { - for (Row aggregateRow : aggregateRows) { - AggregateMetadata aggregate = - AggregateMetadata.build(keyspace, aggregateRow, cassandraVersion, cluster); - if (aggregate != null) { - String name = - Metadata.fullFunctionName(aggregate.getSimpleName(), aggregate.getArgumentTypes()); - aggregates.put(name, aggregate); - } - } - } - return aggregates; - } - - private Map buildViews( - KeyspaceMetadata keyspace, - List viewRows, - Map> colsDefs, - VersionNumber cassandraVersion, - Cluster cluster) { - Map views = - new LinkedHashMap(); - if (viewRows != null) { - for (Row viewRow : viewRows) { - String viewName = viewRow.getString("view_name"); - try { - Map cols = colsDefs.get(viewName); - if (cols == null || cols.isEmpty()) - continue; // we probably raced, we will update the metadata next time - - MaterializedViewMetadata view = - MaterializedViewMetadata.build(keyspace, viewRow, cols, cassandraVersion, cluster); - if (view != null) views.put(view.getName(), view); - } catch (RuntimeException e) { - // See #refresh for why we'd rather not propagate this further - logger.error( - String.format( - "Error parsing schema for view %s.%s: " - + "Cluster.getMetadata().getKeyspace(\"%s\").getView(\"%s\") will be missing or incomplete", - keyspace.getName(), viewName, keyspace.getName(), viewName), - e); - } - } - } - return views; - } - - // Update oldKeyspaces with the changes contained in newKeyspaces. - // This method also takes care of triggering the relevant events - private void updateKeyspaces( - Metadata metadata, - Map oldKeyspaces, - Map newKeyspaces, - String keyspaceToRebuild) { - Iterator it = oldKeyspaces.values().iterator(); - while (it.hasNext()) { - KeyspaceMetadata oldKeyspace = it.next(); - String keyspaceName = oldKeyspace.getName(); - // If we're rebuilding only a single keyspace, we should only consider that one - // because newKeyspaces will only contain that keyspace. - if ((keyspaceToRebuild == null || keyspaceToRebuild.equals(keyspaceName)) - && !newKeyspaces.containsKey(keyspaceName)) { - it.remove(); - metadata.triggerOnKeyspaceRemoved(oldKeyspace); - } - } - for (KeyspaceMetadata newKeyspace : newKeyspaces.values()) { - KeyspaceMetadata oldKeyspace = oldKeyspaces.put(newKeyspace.getName(), newKeyspace); - if (oldKeyspace == null) { - metadata.triggerOnKeyspaceAdded(newKeyspace); - } else if (!oldKeyspace.equals(newKeyspace)) { - metadata.triggerOnKeyspaceChanged(newKeyspace, oldKeyspace); - } - Map oldTables = - oldKeyspace == null - ? new HashMap() - : new HashMap(oldKeyspace.tables); - updateTables(metadata, oldTables, newKeyspace.tables, null); - Map oldTypes = - oldKeyspace == null - ? new HashMap() - : new HashMap(oldKeyspace.userTypes); - updateUserTypes(metadata, oldTypes, newKeyspace.userTypes, null); - Map oldFunctions = - oldKeyspace == null - ? new HashMap() - : new HashMap(oldKeyspace.functions); - updateFunctions(metadata, oldFunctions, newKeyspace.functions, null); - Map oldAggregates = - oldKeyspace == null - ? new HashMap() - : new HashMap(oldKeyspace.aggregates); - updateAggregates(metadata, oldAggregates, newKeyspace.aggregates, null); - Map oldViews = - oldKeyspace == null - ? new HashMap() - : new HashMap(oldKeyspace.views); - updateViews(metadata, oldViews, newKeyspace.views, null); - } - } - - private void updateTables( - Metadata metadata, - Map oldTables, - Map newTables, - String tableToRebuild) { - Iterator it = oldTables.values().iterator(); - while (it.hasNext()) { - TableMetadata oldTable = it.next(); - String tableName = oldTable.getName(); - // If we're rebuilding only a single table, we should only consider that one - // because newTables will only contain that table. - if ((tableToRebuild == null || tableToRebuild.equals(tableName)) - && !newTables.containsKey(tableName)) { - it.remove(); - metadata.triggerOnTableRemoved(oldTable); - } - } - for (TableMetadata newTable : newTables.values()) { - TableMetadata oldTable = oldTables.put(newTable.getName(), newTable); - if (oldTable == null) { - metadata.triggerOnTableAdded(newTable); - } else { - // if we're updating a table only, we need to copy views from old table to the new table. - if (tableToRebuild != null) { - for (MaterializedViewMetadata view : oldTable.getViews()) { - view.setBaseTable(newTable); - } - } - if (!oldTable.equals(newTable)) { - metadata.triggerOnTableChanged(newTable, oldTable); - } - } - } - } - - private void updateUserTypes( - Metadata metadata, - Map oldTypes, - Map newTypes, - String typeToRebuild) { - Iterator it = oldTypes.values().iterator(); - while (it.hasNext()) { - UserType oldType = it.next(); - String typeName = oldType.getTypeName(); - if ((typeToRebuild == null || typeToRebuild.equals(typeName)) - && !newTypes.containsKey(typeName)) { - it.remove(); - metadata.triggerOnUserTypeRemoved(oldType); - } - } - for (UserType newType : newTypes.values()) { - UserType oldType = oldTypes.put(newType.getTypeName(), newType); - if (oldType == null) { - metadata.triggerOnUserTypeAdded(newType); - } else if (!newType.equals(oldType)) { - metadata.triggerOnUserTypeChanged(newType, oldType); - } - } - } - - private void updateFunctions( - Metadata metadata, - Map oldFunctions, - Map newFunctions, - String functionToRebuild) { - Iterator it = oldFunctions.values().iterator(); - while (it.hasNext()) { - FunctionMetadata oldFunction = it.next(); - String oldFunctionName = - Metadata.fullFunctionName( - oldFunction.getSimpleName(), oldFunction.getArguments().values()); - if ((functionToRebuild == null || functionToRebuild.equals(oldFunctionName)) - && !newFunctions.containsKey(oldFunctionName)) { - it.remove(); - metadata.triggerOnFunctionRemoved(oldFunction); - } - } - for (FunctionMetadata newFunction : newFunctions.values()) { - String newFunctionName = - Metadata.fullFunctionName( - newFunction.getSimpleName(), newFunction.getArguments().values()); - FunctionMetadata oldFunction = oldFunctions.put(newFunctionName, newFunction); - if (oldFunction == null) { - metadata.triggerOnFunctionAdded(newFunction); - } else if (!newFunction.equals(oldFunction)) { - metadata.triggerOnFunctionChanged(newFunction, oldFunction); - } - } - } - - private void updateAggregates( - Metadata metadata, - Map oldAggregates, - Map newAggregates, - String aggregateToRebuild) { - Iterator it = oldAggregates.values().iterator(); - while (it.hasNext()) { - AggregateMetadata oldAggregate = it.next(); - String oldAggregateName = - Metadata.fullFunctionName(oldAggregate.getSimpleName(), oldAggregate.getArgumentTypes()); - if ((aggregateToRebuild == null || aggregateToRebuild.equals(oldAggregateName)) - && !newAggregates.containsKey(oldAggregateName)) { - it.remove(); - metadata.triggerOnAggregateRemoved(oldAggregate); - } - } - for (AggregateMetadata newAggregate : newAggregates.values()) { - String newAggregateName = - Metadata.fullFunctionName(newAggregate.getSimpleName(), newAggregate.getArgumentTypes()); - AggregateMetadata oldAggregate = oldAggregates.put(newAggregateName, newAggregate); - if (oldAggregate == null) { - metadata.triggerOnAggregateAdded(newAggregate); - } else if (!newAggregate.equals(oldAggregate)) { - metadata.triggerOnAggregateChanged(newAggregate, oldAggregate); - } - } - } - - private void updateViews( - Metadata metadata, - Map oldViews, - Map newViews, - String viewToRebuild) { - Iterator it = oldViews.values().iterator(); - while (it.hasNext()) { - MaterializedViewMetadata oldView = it.next(); - String aggregateName = oldView.getName(); - if ((viewToRebuild == null || viewToRebuild.equals(aggregateName)) - && !newViews.containsKey(aggregateName)) { - it.remove(); - metadata.triggerOnMaterializedViewRemoved(oldView); - } - } - for (MaterializedViewMetadata newView : newViews.values()) { - MaterializedViewMetadata oldView = oldViews.put(newView.getName(), newView); - if (oldView == null) { - metadata.triggerOnMaterializedViewAdded(newView); - } else if (!newView.equals(oldView)) { - metadata.triggerOnMaterializedViewChanged(newView, oldView); - } - } - } - - static Map> groupByKeyspace(ResultSet rs) { - if (rs == null) return Collections.emptyMap(); - - Map> result = new HashMap>(); - for (Row row : rs) { - String ksName = row.getString(KeyspaceMetadata.KS_NAME); - List l = result.get(ksName); - if (l == null) { - l = new ArrayList(); - result.put(ksName, l); - } - l.add(row); - } - return result; - } - - static Map>> groupByKeyspaceAndCf(ResultSet rs, String tableName) { - if (rs == null) return Collections.emptyMap(); - - Map>> result = Maps.newHashMap(); - for (Row row : rs) { - String ksName = row.getString(KeyspaceMetadata.KS_NAME); - String cfName = row.getString(tableName); - Map> rowsByCf = result.get(ksName); - if (rowsByCf == null) { - rowsByCf = Maps.newHashMap(); - result.put(ksName, rowsByCf); - } - List l = rowsByCf.get(cfName); - if (l == null) { - l = Lists.newArrayList(); - rowsByCf.put(cfName, l); - } - l.add(row); - } - return result; - } - - static Map>> groupByKeyspaceAndCf( - ResultSet rs, VersionNumber cassandraVersion, String tableName) { - if (rs == null) return Collections.emptyMap(); - - Map>> result = - new HashMap>>(); - for (Row row : rs) { - String ksName = row.getString(KeyspaceMetadata.KS_NAME); - String cfName = row.getString(tableName); - Map> colsByCf = result.get(ksName); - if (colsByCf == null) { - colsByCf = new HashMap>(); - result.put(ksName, colsByCf); - } - Map l = colsByCf.get(cfName); - if (l == null) { - l = new HashMap(); - colsByCf.put(cfName, l); - } - ColumnMetadata.Raw c = ColumnMetadata.Raw.fromRow(row, cassandraVersion); - l.put(c.name, c); - } - return result; - } - - private static ResultSetFuture queryAsync( - String query, Connection connection, ProtocolVersion protocolVersion) - throws ConnectionException, BusyConnectionException { - DefaultResultSetFuture future = - new DefaultResultSetFuture(null, protocolVersion, new Requests.Query(query)); - connection.write(future); - return future; - } - - private static ResultSet get(ResultSetFuture future) - throws InterruptedException, ExecutionException { - return (future == null) ? null : future.get(); - } - - /** - * The rows from the system tables that we want to parse to metadata classes. The format of these - * rows depends on the Cassandra version, but our parsing code knows how to handle the - * differences. - */ - private static class SystemRows { - final ResultSet keyspaces; - final Map> tables; - final Map>> columns; - final Map> udts; - final Map> functions; - final Map> aggregates; - final Map> views; - final Map>> indexes; - final ResultSet virtualKeyspaces; - final Map> virtualTables; - final Map>> virtualColumns; - - public SystemRows( - ResultSet keyspaces, - Map> tables, - Map>> columns, - Map> udts, - Map> functions, - Map> aggregates, - Map> views, - Map>> indexes, - ResultSet virtualKeyspaces, - Map> virtualTables, - Map>> virtualColumns) { - this.keyspaces = keyspaces; - this.tables = tables; - this.columns = columns; - this.udts = udts; - this.functions = functions; - this.aggregates = aggregates; - this.views = views; - this.indexes = indexes; - this.virtualKeyspaces = virtualKeyspaces; - this.virtualTables = virtualTables; - this.virtualColumns = virtualColumns; - } - } - - private static class V2SchemaParser extends SchemaParser { - - private static final String SELECT_KEYSPACES = "SELECT * FROM system.schema_keyspaces"; - private static final String SELECT_COLUMN_FAMILIES = - "SELECT * FROM system.schema_columnfamilies"; - private static final String SELECT_COLUMNS = "SELECT * FROM system.schema_columns"; - private static final String SELECT_USERTYPES = "SELECT * FROM system.schema_usertypes"; - private static final String SELECT_FUNCTIONS = "SELECT * FROM system.schema_functions"; - private static final String SELECT_AGGREGATES = "SELECT * FROM system.schema_aggregates"; - - private static final String CF_NAME = "columnfamily_name"; - - @Override - SystemRows fetchSystemRows( - Cluster cluster, - SchemaElement targetType, - String targetKeyspace, - String targetName, - List targetSignature, - Connection connection, - VersionNumber cassandraVersion) - throws ConnectionException, BusyConnectionException, ExecutionException, - InterruptedException { - - boolean isSchemaOrKeyspace = (targetType == null || targetType == KEYSPACE); - - String whereClause = ""; - if (targetType != null) { - whereClause = " WHERE keyspace_name = '" + targetKeyspace + '\''; - if (targetType == TABLE) whereClause += " AND columnfamily_name = '" + targetName + '\''; - else if (targetType == TYPE) whereClause += " AND type_name = '" + targetName + '\''; - else if (targetType == FUNCTION) - whereClause += - " AND function_name = '" - + targetName - + "' AND signature = " - + LIST_OF_TEXT_CODEC.format(targetSignature); - else if (targetType == AGGREGATE) - whereClause += - " AND aggregate_name = '" - + targetName - + "' AND signature = " - + LIST_OF_TEXT_CODEC.format(targetSignature); - } - - ResultSetFuture ksFuture = null, - udtFuture = null, - cfFuture = null, - colsFuture = null, - functionsFuture = null, - aggregatesFuture = null; - - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - - if (isSchemaOrKeyspace) - ksFuture = queryAsync(SELECT_KEYSPACES + whereClause, connection, protocolVersion); - - if (isSchemaOrKeyspace && supportsUdts(cassandraVersion) || targetType == TYPE) - udtFuture = queryAsync(SELECT_USERTYPES + whereClause, connection, protocolVersion); - - if (isSchemaOrKeyspace || targetType == TABLE) { - cfFuture = queryAsync(SELECT_COLUMN_FAMILIES + whereClause, connection, protocolVersion); - colsFuture = queryAsync(SELECT_COLUMNS + whereClause, connection, protocolVersion); - } - - if ((isSchemaOrKeyspace && supportsUdfs(cassandraVersion) || targetType == FUNCTION)) - functionsFuture = queryAsync(SELECT_FUNCTIONS + whereClause, connection, protocolVersion); - - if (isSchemaOrKeyspace && supportsUdfs(cassandraVersion) || targetType == AGGREGATE) - aggregatesFuture = queryAsync(SELECT_AGGREGATES + whereClause, connection, protocolVersion); - - return new SystemRows( - get(ksFuture), - groupByKeyspace(get(cfFuture)), - groupByKeyspaceAndCf(get(colsFuture), cassandraVersion, CF_NAME), - groupByKeyspace(get(udtFuture)), - groupByKeyspace(get(functionsFuture)), - groupByKeyspace(get(aggregatesFuture)), - // No views nor separate indexes table in Cassandra 2: - Collections.>emptyMap(), - Collections.>>emptyMap(), - null, - Collections.>emptyMap(), - Collections.>>emptyMap()); - } - - @Override - String tableNameColumn() { - return CF_NAME; - } - - private boolean supportsUdts(VersionNumber cassandraVersion) { - return cassandraVersion.getMajor() > 2 - || (cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() >= 1); - } - - private boolean supportsUdfs(VersionNumber cassandraVersion) { - return cassandraVersion.getMajor() > 2 - || (cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() >= 2); - } - } - - private static class V3SchemaParser extends SchemaParser { - - protected static final String SELECT_KEYSPACES = "SELECT * FROM system_schema.keyspaces"; - protected static final String SELECT_TABLES = "SELECT * FROM system_schema.tables"; - protected static final String SELECT_COLUMNS = "SELECT * FROM system_schema.columns"; - protected static final String SELECT_USERTYPES = "SELECT * FROM system_schema.types"; - protected static final String SELECT_FUNCTIONS = "SELECT * FROM system_schema.functions"; - protected static final String SELECT_AGGREGATES = "SELECT * FROM system_schema.aggregates"; - protected static final String SELECT_INDEXES = "SELECT * FROM system_schema.indexes"; - protected static final String SELECT_VIEWS = "SELECT * FROM system_schema.views"; - - private static final String TABLE_NAME = "table_name"; - - @Override - SystemRows fetchSystemRows( - Cluster cluster, - SchemaElement targetType, - String targetKeyspace, - String targetName, - List targetSignature, - Connection connection, - VersionNumber cassandraVersion) - throws ConnectionException, BusyConnectionException, ExecutionException, - InterruptedException { - - boolean isSchemaOrKeyspace = (targetType == null || targetType == KEYSPACE); - - ResultSetFuture ksFuture = null, - udtFuture = null, - cfFuture = null, - colsFuture = null, - functionsFuture = null, - aggregatesFuture = null, - indexesFuture = null, - viewsFuture = null; - - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - - if (isSchemaOrKeyspace) - ksFuture = - queryAsync( - SELECT_KEYSPACES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - - if (isSchemaOrKeyspace || targetType == TYPE) - udtFuture = - queryAsync( - SELECT_USERTYPES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - - if (isSchemaOrKeyspace || targetType == TABLE) { - cfFuture = - queryAsync( - SELECT_TABLES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - colsFuture = - queryAsync( - SELECT_COLUMNS - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - indexesFuture = - queryAsync( - SELECT_INDEXES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - viewsFuture = - queryAsync( - SELECT_VIEWS - + whereClause( - targetType == TABLE ? VIEW : targetType, - targetKeyspace, - targetName, - targetSignature), - connection, - protocolVersion); - } - - if (isSchemaOrKeyspace || targetType == FUNCTION) - functionsFuture = - queryAsync( - SELECT_FUNCTIONS - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - - if (isSchemaOrKeyspace || targetType == AGGREGATE) - aggregatesFuture = - queryAsync( - SELECT_AGGREGATES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - - return new SystemRows( - get(ksFuture), - groupByKeyspace(get(cfFuture)), - groupByKeyspaceAndCf(get(colsFuture), cassandraVersion, TABLE_NAME), - groupByKeyspace(get(udtFuture)), - groupByKeyspace(get(functionsFuture)), - groupByKeyspace(get(aggregatesFuture)), - groupByKeyspace(get(viewsFuture)), - groupByKeyspaceAndCf(get(indexesFuture), TABLE_NAME), - null, - Collections.>emptyMap(), - Collections.>>emptyMap()); - } - - @Override - String tableNameColumn() { - return TABLE_NAME; - } - - protected String whereClause( - SchemaElement targetType, - String targetKeyspace, - String targetName, - List targetSignature) { - String whereClause = ""; - if (targetType != null) { - whereClause = " WHERE keyspace_name = '" + targetKeyspace + '\''; - if (targetType == TABLE) whereClause += " AND table_name = '" + targetName + '\''; - else if (targetType == VIEW) whereClause += " AND view_name = '" + targetName + '\''; - else if (targetType == TYPE) whereClause += " AND type_name = '" + targetName + '\''; - else if (targetType == FUNCTION) - whereClause += - " AND function_name = '" - + targetName - + "' AND argument_types = " - + LIST_OF_TEXT_CODEC.format(targetSignature); - else if (targetType == AGGREGATE) - whereClause += - " AND aggregate_name = '" - + targetName - + "' AND argument_types = " - + LIST_OF_TEXT_CODEC.format(targetSignature); - } - return whereClause; - } - - // Used by maybeSortUdts to sort at each dependency group alphabetically. - private static final Comparator sortByTypeName = - new Comparator() { - @Override - public int compare(Row o1, Row o2) { - String type1 = o1.getString(UserType.TYPE_NAME); - String type2 = o2.getString(UserType.TYPE_NAME); - - if (type1 == null && type2 == null) { - return 0; - } else if (type2 == null) { - return 1; - } else if (type1 == null) { - return -1; - } else { - return type1.compareTo(type2); - } - } - }; - - @Override - protected List maybeSortUdts(List udtRows, Cluster cluster, String keyspace) { - if (udtRows.size() < 2) return udtRows; - - // For C* 3+, user-defined type resolution must be done in proper order - // to guarantee that nested UDTs get resolved - DirectedGraph graph = new DirectedGraph(sortByTypeName, udtRows); - for (Row from : udtRows) { - for (Row to : udtRows) { - if (from != to && dependsOn(to, from, cluster, keyspace)) graph.addEdge(from, to); - } - } - return graph.topologicalSort(); - } - - private boolean dependsOn(Row udt1, Row udt2, Cluster cluster, String keyspace) { - List fieldTypes = udt1.getList(UserType.COLS_TYPES, String.class); - String typeName = udt2.getString(UserType.TYPE_NAME); - for (String fieldTypeStr : fieldTypes) { - // use shallow user types since some definitions might not be known at this stage - DataType fieldType = - DataTypeCqlNameParser.parse(fieldTypeStr, cluster, keyspace, null, null, false, true); - if (references(fieldType, typeName)) return true; - } - return false; - } - - private boolean references(DataType dataType, String typeName) { - if (dataType instanceof UserType.Shallow - && ((UserType.Shallow) dataType).typeName.equals(typeName)) return true; - for (DataType arg : dataType.getTypeArguments()) { - if (references(arg, typeName)) return true; - } - if (dataType instanceof TupleType) { - for (DataType arg : ((TupleType) dataType).getComponentTypes()) { - if (references(arg, typeName)) return true; - } - } - return false; - } - } - - private static class V4SchemaParser extends V3SchemaParser { - - private static final String SELECT_VIRTUAL_KEYSPACES = - "SELECT * FROM system_virtual_schema.keyspaces"; - private static final String SELECT_VIRTUAL_TABLES = - "SELECT * FROM system_virtual_schema.tables"; - private static final String SELECT_VIRTUAL_COLUMNS = - "SELECT * FROM system_virtual_schema.columns"; - - private static final String TABLE_NAME = "table_name"; - - @Override - SystemRows fetchSystemRows( - Cluster cluster, - SchemaElement targetType, - String targetKeyspace, - String targetName, - List targetSignature, - Connection connection, - VersionNumber cassandraVersion) - throws ConnectionException, BusyConnectionException, ExecutionException, - InterruptedException { - - boolean isSchemaOrKeyspace = (targetType == null || targetType == KEYSPACE); - - ResultSetFuture ksFuture = null, - udtFuture = null, - cfFuture = null, - colsFuture = null, - functionsFuture = null, - aggregatesFuture = null, - indexesFuture = null, - viewsFuture = null, - virtualKeyspacesFuture = null, - virtualTableFuture = null, - virtualColumnsFuture = null; - - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - - if (isSchemaOrKeyspace) { - ksFuture = - queryAsync( - SELECT_KEYSPACES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - virtualKeyspacesFuture = - queryAsync( - SELECT_VIRTUAL_KEYSPACES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - virtualColumnsFuture = - queryAsync( - SELECT_VIRTUAL_COLUMNS - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - virtualTableFuture = - queryAsync( - SELECT_VIRTUAL_TABLES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - } - - if (isSchemaOrKeyspace || targetType == TYPE) { - udtFuture = - queryAsync( - SELECT_USERTYPES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - } - - if (isSchemaOrKeyspace || targetType == TABLE) { - cfFuture = - queryAsync( - SELECT_TABLES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - colsFuture = - queryAsync( - SELECT_COLUMNS - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - indexesFuture = - queryAsync( - SELECT_INDEXES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - viewsFuture = - queryAsync( - SELECT_VIEWS - + whereClause( - targetType == TABLE ? VIEW : targetType, - targetKeyspace, - targetName, - targetSignature), - connection, - protocolVersion); - } - - if (isSchemaOrKeyspace || targetType == FUNCTION) { - functionsFuture = - queryAsync( - SELECT_FUNCTIONS - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - } - - if (isSchemaOrKeyspace || targetType == AGGREGATE) { - aggregatesFuture = - queryAsync( - SELECT_AGGREGATES - + whereClause(targetType, targetKeyspace, targetName, targetSignature), - connection, - protocolVersion); - } - - return new SystemRows( - get(ksFuture), - groupByKeyspace(get(cfFuture)), - groupByKeyspaceAndCf(get(colsFuture), cassandraVersion, TABLE_NAME), - groupByKeyspace(get(udtFuture)), - groupByKeyspace(get(functionsFuture)), - groupByKeyspace(get(aggregatesFuture)), - groupByKeyspace(get(viewsFuture)), - groupByKeyspaceAndCf(get(indexesFuture), TABLE_NAME), - get(virtualKeyspacesFuture), - groupByKeyspace(get(virtualTableFuture)), - groupByKeyspaceAndCf(get(virtualColumnsFuture), cassandraVersion, TABLE_NAME)); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Segment.java b/driver-core/src/main/java/com/datastax/driver/core/Segment.java deleted file mode 100644 index 231c50028f6..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Segment.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.buffer.ByteBuf; - -/** - * A container of {@link Frame}s in protocol v5 and above. This is a new protocol construct that - * allows checksumming and compressing multiple messages together. - * - *

{@link #getPayload()} contains either: - * - *

    - *
  • a sequence of encoded {@link Frame}s, all concatenated together. In this case, {@link - * #isSelfContained()} return true. - *
  • or a slice of an encoded large {@link Frame} (if that frame is longer than {@link - * #MAX_PAYLOAD_LENGTH}). In this case, {@link #isSelfContained()} returns false. - *
- * - * The payload is not compressed; compression is handled at a lower level when encoding or decoding - * this object. - * - *

Naming is provisional: "segment" is not the official name, I picked it arbitrarily for the - * driver code to avoid a name clash. It's possible that this type will be renamed to "frame", and - * {@link Frame} to something else, at some point in the future (this is an ongoing discussion on - * the server ticket). - */ -class Segment { - - static int MAX_PAYLOAD_LENGTH = 128 * 1024 - 1; - - private final ByteBuf payload; - private final boolean isSelfContained; - - Segment(ByteBuf payload, boolean isSelfContained) { - this.payload = payload; - this.isSelfContained = isSelfContained; - } - - public ByteBuf getPayload() { - return payload; - } - - public boolean isSelfContained() { - return isSelfContained; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SegmentBuilder.java b/driver-core/src/main/java/com/datastax/driver/core/SegmentBuilder.java deleted file mode 100644 index ddf0235b48e..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SegmentBuilder.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.GenericFutureListener; -import java.util.ArrayList; -import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Abstracts the details of batching a sequence of {@link Message.Request}s into one or more {@link - * Segment}s before sending them out on the network. - * - *

This class is not thread-safe. - */ -class SegmentBuilder { - - private static final Logger logger = LoggerFactory.getLogger(SegmentBuilder.class); - - private final ChannelHandlerContext context; - private final ByteBufAllocator allocator; - private final int maxPayloadLength; - private final Message.ProtocolEncoder requestEncoder; - - private final List currentPayloadHeaders = new ArrayList(); - private final List currentPayloadBodies = new ArrayList(); - private final List currentPayloadPromises = new ArrayList(); - private int currentPayloadLength; - - SegmentBuilder( - ChannelHandlerContext context, - ByteBufAllocator allocator, - Message.ProtocolEncoder requestEncoder) { - this(context, allocator, requestEncoder, Segment.MAX_PAYLOAD_LENGTH); - } - - /** Exposes the max length for unit tests; in production, this is hard-coded. */ - @VisibleForTesting - SegmentBuilder( - ChannelHandlerContext context, - ByteBufAllocator allocator, - Message.ProtocolEncoder requestEncoder, - int maxPayloadLength) { - this.context = context; - this.allocator = allocator; - this.requestEncoder = requestEncoder; - this.maxPayloadLength = maxPayloadLength; - } - - /** - * Adds a new request. It will be encoded into one or more segments, that will be passed to {@link - * #processSegment(Segment, ChannelPromise)} at some point in the future. - * - *

The caller must invoke {@link #flush()} after the last request. - */ - public void addRequest(Message.Request request, ChannelPromise promise) { - - // Wrap the request into a legacy frame, append that frame to the payload. - int frameHeaderLength = Frame.Header.lengthFor(requestEncoder.protocolVersion); - int frameBodyLength = requestEncoder.encodedSize(request); - int frameLength = frameHeaderLength + frameBodyLength; - - Frame.Header header = - new Frame.Header( - requestEncoder.protocolVersion, - requestEncoder.computeFlags(request), - request.getStreamId(), - request.type.opcode, - frameBodyLength); - - if (frameLength > maxPayloadLength) { - // Large request: split into multiple dedicated segments and process them immediately: - ByteBuf frame = allocator.ioBuffer(frameLength); - header.encodeInto(frame); - requestEncoder.encode(request, frame); - - int sliceCount = - (frameLength / maxPayloadLength) + (frameLength % maxPayloadLength == 0 ? 0 : 1); - - logger.trace( - "Splitting large request ({} bytes) into {} segments: {}", - frameLength, - sliceCount, - request); - - List segmentPromises = split(promise, sliceCount); - int i = 0; - do { - ByteBuf part = frame.readSlice(Math.min(maxPayloadLength, frame.readableBytes())); - part.retain(); - process(part, false, segmentPromises.get(i++)); - } while (frame.isReadable()); - // We've retained each slice, and won't reference this buffer anymore - frame.release(); - } else { - // Small request: append to an existing segment, together with other messages. - if (currentPayloadLength + frameLength > maxPayloadLength) { - // Current segment is full, process and start a new one: - processCurrentPayload(); - resetCurrentPayload(); - } - // Append frame to current segment - logger.trace( - "Adding {}th request to self-contained segment: {}", - currentPayloadHeaders.size() + 1, - request); - currentPayloadHeaders.add(header); - currentPayloadBodies.add(request); - currentPayloadPromises.add(promise); - currentPayloadLength += frameLength; - } - } - - /** - * Signals that we're done adding requests. - * - *

This must be called after adding the last request, it will possibly trigger the generation - * of one last segment. - */ - public void flush() { - if (currentPayloadLength > 0) { - processCurrentPayload(); - resetCurrentPayload(); - } - } - - /** What to do whenever a full segment is ready. */ - protected void processSegment(Segment segment, ChannelPromise segmentPromise) { - context.write(segment, segmentPromise); - } - - private void process(ByteBuf payload, boolean isSelfContained, ChannelPromise segmentPromise) { - processSegment(new Segment(payload, isSelfContained), segmentPromise); - } - - private void processCurrentPayload() { - int requestCount = currentPayloadHeaders.size(); - assert currentPayloadBodies.size() == requestCount - && currentPayloadPromises.size() == requestCount; - logger.trace("Emitting new self-contained segment with {} frame(s)", requestCount); - ByteBuf payload = this.allocator.ioBuffer(currentPayloadLength); - for (int i = 0; i < requestCount; i++) { - Frame.Header header = currentPayloadHeaders.get(i); - Message.Request request = currentPayloadBodies.get(i); - header.encodeInto(payload); - requestEncoder.encode(request, payload); - } - process(payload, true, merge(currentPayloadPromises)); - } - - private void resetCurrentPayload() { - currentPayloadHeaders.clear(); - currentPayloadBodies.clear(); - currentPayloadPromises.clear(); - currentPayloadLength = 0; - } - - // Merges multiple promises into a single one, that will notify all of them when done. - // This is used when multiple requests are sent as a single segment. - private ChannelPromise merge(List framePromises) { - if (framePromises.size() == 1) { - return framePromises.get(0); - } - ChannelPromise segmentPromise = context.newPromise(); - final ImmutableList dependents = ImmutableList.copyOf(framePromises); - segmentPromise.addListener( - new GenericFutureListener>() { - @Override - public void operationComplete(Future future) throws Exception { - if (future.isSuccess()) { - for (ChannelPromise framePromise : dependents) { - framePromise.setSuccess(); - } - } else { - Throwable cause = future.cause(); - for (ChannelPromise framePromise : dependents) { - framePromise.setFailure(cause); - } - } - } - }); - return segmentPromise; - } - - // Splits a single promise into multiple ones. The original promise will complete when all the - // splits have. - // This is used when a single request is sliced into multiple segment. - private List split(ChannelPromise framePromise, int sliceCount) { - // We split one frame into multiple slices. When all slices are written, the frame is written. - List slicePromises = new ArrayList(sliceCount); - for (int i = 0; i < sliceCount; i++) { - slicePromises.add(context.newPromise()); - } - GenericFutureListener> sliceListener = - new SliceWriteListener(framePromise, slicePromises); - for (int i = 0; i < sliceCount; i++) { - slicePromises.get(i).addListener(sliceListener); - } - return slicePromises; - } - - static class SliceWriteListener implements GenericFutureListener> { - - private final ChannelPromise parentPromise; - private final List slicePromises; - - // All slices are written to the same channel, and the segment is built from the Flusher which - // also runs on the same event loop, so we don't need synchronization. - private int remainingSlices; - - SliceWriteListener(ChannelPromise parentPromise, List slicePromises) { - this.parentPromise = parentPromise; - this.slicePromises = slicePromises; - this.remainingSlices = slicePromises.size(); - } - - @Override - public void operationComplete(Future future) { - if (!parentPromise.isDone()) { - if (future.isSuccess()) { - remainingSlices -= 1; - if (remainingSlices == 0) { - parentPromise.setSuccess(); - } - } else { - // If any slice fails, we can immediately mark the whole frame as failed: - parentPromise.setFailure(future.cause()); - // Cancel any remaining slice, Netty will not send the bytes. - for (ChannelPromise slicePromise : slicePromises) { - slicePromise.cancel(/*Netty ignores this*/ false); - } - } - } - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SegmentCodec.java b/driver-core/src/main/java/com/datastax/driver/core/SegmentCodec.java deleted file mode 100644 index 66122e4725f..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SegmentCodec.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.CrcMismatchException; -import com.google.common.annotations.VisibleForTesting; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import java.io.IOException; -import java.util.List; - -class SegmentCodec { - - private static final int COMPRESSED_HEADER_LENGTH = 5; - private static final int UNCOMPRESSED_HEADER_LENGTH = 3; - static final int CRC24_LENGTH = 3; - static final int CRC32_LENGTH = 4; - - private final ByteBufAllocator allocator; - private final boolean compress; - private final FrameCompressor compressor; - - SegmentCodec(ByteBufAllocator allocator, ProtocolOptions.Compression compression) { - this.allocator = allocator; - this.compress = compression != ProtocolOptions.Compression.NONE; - this.compressor = compression.compressor(); - } - - /** The length of the segment header, excluding the 3-byte trailing CRC. */ - int headerLength() { - return compress ? COMPRESSED_HEADER_LENGTH : UNCOMPRESSED_HEADER_LENGTH; - } - - void encode(Segment segment, List out) throws IOException { - ByteBuf uncompressedPayload = segment.getPayload(); - int uncompressedPayloadLength = uncompressedPayload.readableBytes(); - assert uncompressedPayloadLength <= Segment.MAX_PAYLOAD_LENGTH; - ByteBuf encodedPayload; - if (compress) { - uncompressedPayload.markReaderIndex(); - ByteBuf compressedPayload = compressor.compress(uncompressedPayload); - if (compressedPayload.readableBytes() >= uncompressedPayloadLength) { - // Skip compression if it's not worth it - uncompressedPayload.resetReaderIndex(); - encodedPayload = uncompressedPayload; - compressedPayload.release(); - // By convention, this is how we signal this to the server: - uncompressedPayloadLength = 0; - } else { - encodedPayload = compressedPayload; - uncompressedPayload.release(); - } - } else { - encodedPayload = uncompressedPayload; - } - int payloadLength = encodedPayload.readableBytes(); - - ByteBuf header = - encodeHeader(payloadLength, uncompressedPayloadLength, segment.isSelfContained()); - - int payloadCrc = Crc.computeCrc32(encodedPayload); - ByteBuf trailer = allocator.ioBuffer(CRC32_LENGTH); - for (int i = 0; i < CRC32_LENGTH; i++) { - trailer.writeByte(payloadCrc & 0xFF); - payloadCrc >>= 8; - } - - out.add(header); - out.add(encodedPayload); - out.add(trailer); - } - - @VisibleForTesting - ByteBuf encodeHeader(int payloadLength, int uncompressedLength, boolean isSelfContained) { - assert payloadLength <= Segment.MAX_PAYLOAD_LENGTH; - assert !compress || uncompressedLength <= Segment.MAX_PAYLOAD_LENGTH; - - int headerLength = headerLength(); - - long headerData = payloadLength; - int flagOffset = 17; - if (compress) { - headerData |= (long) uncompressedLength << 17; - flagOffset += 17; - } - if (isSelfContained) { - headerData |= 1L << flagOffset; - } - - int headerCrc = Crc.computeCrc24(headerData, headerLength); - - ByteBuf header = allocator.ioBuffer(headerLength + CRC24_LENGTH); - // Write both data and CRC in little-endian order - for (int i = 0; i < headerLength; i++) { - int shift = i * 8; - header.writeByte((int) (headerData >> shift & 0xFF)); - } - for (int i = 0; i < CRC24_LENGTH; i++) { - int shift = i * 8; - header.writeByte(headerCrc >> shift & 0xFF); - } - return header; - } - - /** - * Decodes a segment header and checks its CRC. It is assumed that the caller has already checked - * that there are enough bytes. - */ - Header decodeHeader(ByteBuf buffer) throws CrcMismatchException { - int headerLength = headerLength(); - assert buffer.readableBytes() >= headerLength + CRC24_LENGTH; - - // Read header data (little endian): - long headerData = 0; - for (int i = 0; i < headerLength; i++) { - headerData |= (buffer.readByte() & 0xFFL) << 8 * i; - } - - // Read CRC (little endian) and check it: - int expectedHeaderCrc = 0; - for (int i = 0; i < CRC24_LENGTH; i++) { - expectedHeaderCrc |= (buffer.readByte() & 0xFF) << 8 * i; - } - int actualHeaderCrc = Crc.computeCrc24(headerData, headerLength); - if (actualHeaderCrc != expectedHeaderCrc) { - throw new CrcMismatchException( - String.format( - "CRC mismatch on header %s. Received %s, computed %s.", - Long.toHexString(headerData), - Integer.toHexString(expectedHeaderCrc), - Integer.toHexString(actualHeaderCrc))); - } - - int payloadLength = (int) headerData & Segment.MAX_PAYLOAD_LENGTH; - headerData >>= 17; - int uncompressedPayloadLength; - if (compress) { - uncompressedPayloadLength = (int) headerData & Segment.MAX_PAYLOAD_LENGTH; - headerData >>= 17; - } else { - uncompressedPayloadLength = -1; - } - boolean isSelfContained = (headerData & 1) == 1; - return new Header(payloadLength, uncompressedPayloadLength, isSelfContained); - } - - /** - * Decodes the rest of a segment from a previously decoded header, and checks the payload's CRC. - * It is assumed that the caller has already checked that there are enough bytes. - */ - Segment decode(Header header, ByteBuf buffer) throws CrcMismatchException, IOException { - assert buffer.readableBytes() == header.payloadLength + CRC32_LENGTH; - - // Extract payload: - ByteBuf encodedPayload = buffer.readSlice(header.payloadLength); - encodedPayload.retain(); - - // Read and check CRC: - int expectedPayloadCrc = 0; - for (int i = 0; i < CRC32_LENGTH; i++) { - expectedPayloadCrc |= (buffer.readByte() & 0xFF) << 8 * i; - } - buffer.release(); // done with this (we retained the payload independently) - int actualPayloadCrc = Crc.computeCrc32(encodedPayload); - if (actualPayloadCrc != expectedPayloadCrc) { - encodedPayload.release(); - throw new CrcMismatchException( - String.format( - "CRC mismatch on payload. Received %s, computed %s.", - Integer.toHexString(expectedPayloadCrc), Integer.toHexString(actualPayloadCrc))); - } - - // Decompress payload if needed: - ByteBuf payload; - if (compress && header.uncompressedPayloadLength > 0) { - payload = compressor.decompress(encodedPayload, header.uncompressedPayloadLength); - encodedPayload.release(); - } else { - payload = encodedPayload; - } - - return new Segment(payload, header.isSelfContained); - } - - /** Temporary holder for header data. During decoding, it is convenient to store it separately. */ - static class Header { - final int payloadLength; - final int uncompressedPayloadLength; - final boolean isSelfContained; - - public Header(int payloadLength, int uncompressedPayloadLength, boolean isSelfContained) { - this.payloadLength = payloadLength; - this.uncompressedPayloadLength = uncompressedPayloadLength; - this.isSelfContained = isSelfContained; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SegmentToBytesEncoder.java b/driver-core/src/main/java/com/datastax/driver/core/SegmentToBytesEncoder.java deleted file mode 100644 index f4cd8b42672..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SegmentToBytesEncoder.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToMessageEncoder; -import java.util.List; - -@ChannelHandler.Sharable -class SegmentToBytesEncoder extends MessageToMessageEncoder { - - private final SegmentCodec codec; - - SegmentToBytesEncoder(SegmentCodec codec) { - super(Segment.class); - this.codec = codec; - } - - @Override - protected void encode(ChannelHandlerContext ctx, Segment segment, List out) - throws Exception { - codec.encode(segment, out); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SegmentToFrameDecoder.java b/driver-core/src/main/java/com/datastax/driver/core/SegmentToFrameDecoder.java deleted file mode 100644 index 095d3836960..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SegmentToFrameDecoder.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.Frame.Header; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.CompositeByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToMessageDecoder; -import java.util.ArrayList; -import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Converts the segments decoded by {@link BytesToSegmentDecoder} into legacy frames understood by - * the rest of the driver. - */ -class SegmentToFrameDecoder extends MessageToMessageDecoder { - - private static final Logger logger = LoggerFactory.getLogger(SegmentToFrameDecoder.class); - - // Accumulated state when we are reading a sequence of slices - private Header pendingHeader; - private final List accumulatedSlices = new ArrayList(); - private int accumulatedLength; - - SegmentToFrameDecoder() { - super(Segment.class); - } - - @Override - protected void decode(ChannelHandlerContext ctx, Segment segment, List out) { - if (segment.isSelfContained()) { - decodeSelfContained(segment, out); - } else { - decodeSlice(segment, ctx.alloc(), out); - } - } - - private void decodeSelfContained(Segment segment, List out) { - ByteBuf payload = segment.getPayload(); - int frameCount = 0; - do { - Header header = Header.decode(payload); - ByteBuf body = payload.readSlice(header.bodyLength); - body.retain(); - out.add(new Frame(header, body)); - frameCount += 1; - } while (payload.isReadable()); - payload.release(); - logger.trace("Decoded self-contained segment into {} frame(s)", frameCount); - } - - private void decodeSlice(Segment segment, ByteBufAllocator allocator, List out) { - assert pendingHeader != null ^ (accumulatedSlices.isEmpty() && accumulatedLength == 0); - ByteBuf payload = segment.getPayload(); - if (pendingHeader == null) { // first slice - pendingHeader = Header.decode(payload); // note: this consumes the header data - } - accumulatedSlices.add(payload); - accumulatedLength += payload.readableBytes(); - logger.trace( - "StreamId {}: decoded slice {}, {}/{} bytes", - pendingHeader.streamId, - accumulatedSlices.size(), - accumulatedLength, - pendingHeader.bodyLength); - assert accumulatedLength <= pendingHeader.bodyLength; - if (accumulatedLength == pendingHeader.bodyLength) { - // We've received enough data to reassemble the whole message - CompositeByteBuf body = allocator.compositeBuffer(accumulatedSlices.size()); - body.addComponents(true, accumulatedSlices); - out.add(new Frame(pendingHeader, body)); - // Reset our state - pendingHeader = null; - accumulatedSlices.clear(); - accumulatedLength = 0; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ServerSideTimestampGenerator.java b/driver-core/src/main/java/com/datastax/driver/core/ServerSideTimestampGenerator.java deleted file mode 100644 index 6f54660f36b..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ServerSideTimestampGenerator.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * A timestamp generator that always returns {@link Long#MIN_VALUE}, in order to let Cassandra - * assign server-side timestamps. - */ -public class ServerSideTimestampGenerator implements TimestampGenerator { - /** The unique instance of this generator. */ - public static final TimestampGenerator INSTANCE = new ServerSideTimestampGenerator(); - - @Override - public long next() { - return Long.MIN_VALUE; - } - - private ServerSideTimestampGenerator() {} -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Session.java b/driver-core/src/main/java/com/datastax/driver/core/Session.java deleted file mode 100644 index a8e4a59b2a8..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Session.java +++ /dev/null @@ -1,454 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.AuthenticationException; -import com.datastax.driver.core.exceptions.NoHostAvailableException; -import com.datastax.driver.core.exceptions.QueryExecutionException; -import com.datastax.driver.core.exceptions.QueryValidationException; -import com.datastax.driver.core.exceptions.UnsupportedFeatureException; -import com.google.common.util.concurrent.ListenableFuture; -import java.io.Closeable; -import java.util.Collection; -import java.util.Map; - -/** - * A session holds connections to a Cassandra cluster, allowing it to be queried. - * - *

Each session maintains multiple connections to the cluster nodes, provides policies to choose - * which node to use for each query (round-robin on all nodes of the cluster by default), and - * handles retries for failed queries (when it makes sense), etc... - * - *

Session instances are thread-safe and usually a single instance is enough per application. As - * a given session can only be "logged" into one keyspace at a time (where the "logged" keyspace is - * the one used by queries that don't explicitly use a fully qualified table name), it can make - * sense to create one session per keyspace used. This is however not necessary when querying - * multiple keyspaces since it is always possible to use a single session with fully qualified table - * names in queries. - */ -public interface Session extends Closeable { - - /** - * The keyspace to which this Session is currently logged in, if any. - * - *

This correspond to the name passed to {@link Cluster#connect(String)}, or to the last - * keyspace logged into through a "USE" CQL query if one was used. - * - * @return the name of the keyspace to which this Session is currently logged in, or {@code null} - * if the session is logged to no keyspace. - */ - String getLoggedKeyspace(); - - /** - * Force the initialization of this Session instance if it hasn't been initialized yet. - * - *

Please note first that most users won't need to call this method explicitly. If you use the - * {@link Cluster#connect} method to create your Session, the returned session will be already - * initialized. Even if you create a non-initialized session through {@link Cluster#newSession}, - * that session will get automatically initialized the first time it is used for querying. This - * method is thus only useful if you use {@link Cluster#newSession} and want to explicitly force - * initialization without querying. - * - *

Session initialization consists in connecting the Session to the known Cassandra hosts (at - * least those that should not be ignored due to the {@link - * com.datastax.driver.core.policies.LoadBalancingPolicy LoadBalancingPolicy} in place). - * - *

If the Cluster instance this Session depends on is not itself initialized, it will be - * initialized by this method. - * - *

If the session is already initialized, this method is a no-op. - * - * @return this {@code Session} object. - * @throws NoHostAvailableException if this initialization triggers the {@link Cluster} - * initialization and no host amongst the contact points can be reached. - * @throws AuthenticationException if this initialization triggers the {@link Cluster} - * initialization and an authentication error occurs while contacting the initial contact - * points. - */ - Session init(); - - /** - * Initialize this session asynchronously. - * - * @return a future that will complete when the session is fully initialized. - * @see #init() - */ - ListenableFuture initAsync(); - - /** - * Executes the provided query. - * - *

This is a convenience method for {@code execute(new SimpleStatement(query))}. - * - * @param query the CQL query to execute. - * @return the result of the query. That result will never be null but can be empty (and will be - * for any non SELECT query). - * @throws NoHostAvailableException if no host in the cluster can be contacted successfully to - * execute this query. - * @throws QueryExecutionException if the query triggered an execution exception, i.e. an - * exception thrown by Cassandra when it cannot execute the query with the requested - * consistency level successfully. - * @throws QueryValidationException if the query if invalid (syntax error, unauthorized or any - * other validation problem). - */ - ResultSet execute(String query); - - /** - * Executes the provided query using the provided values. - * - *

This is a convenience method for {@code execute(new SimpleStatement(query, values))}. - * - * @param query the CQL query to execute. - * @param values values required for the execution of {@code query}. See {@link - * SimpleStatement#SimpleStatement(String, Object...)} for more details. - * @return the result of the query. That result will never be null but can be empty (and will be - * for any non SELECT query). - * @throws NoHostAvailableException if no host in the cluster can be contacted successfully to - * execute this query. - * @throws QueryExecutionException if the query triggered an execution exception, i.e. an - * exception thrown by Cassandra when it cannot execute the query with the requested - * consistency level successfully. - * @throws QueryValidationException if the query if invalid (syntax error, unauthorized or any - * other validation problem). - * @throws UnsupportedFeatureException if version 1 of the protocol is in use (i.e. if you've - * forced version 1 through {@link Cluster.Builder#withProtocolVersion} or you use Cassandra - * 1.2). - */ - ResultSet execute(String query, Object... values); - - /** - * Executes the provided query using the provided named values. - * - *

This is a convenience method for {@code execute(new SimpleStatement(query, values))}. - * - * @param query the CQL query to execute. - * @param values values required for the execution of {@code query}. See {@link - * SimpleStatement#SimpleStatement(String, Map)} for more details. - * @return the result of the query. That result will never be null but can be empty (and will be - * for any non SELECT query). - * @throws NoHostAvailableException if no host in the cluster can be contacted successfully to - * execute this query. - * @throws QueryExecutionException if the query triggered an execution exception, i.e. an - * exception thrown by Cassandra when it cannot execute the query with the requested - * consistency level successfully. - * @throws QueryValidationException if the query if invalid (syntax error, unauthorized or any - * other validation problem). - * @throws UnsupportedFeatureException if version 1 or 2 of the protocol is in use (i.e. if you've - * forced it through {@link Cluster.Builder#withProtocolVersion} or you use Cassandra 1.2 or - * 2.0). - */ - ResultSet execute(String query, Map values); - - /** - * Executes the provided query. - * - *

This method blocks until at least some result has been received from the database. However, - * for SELECT queries, it does not guarantee that the result has been received in full. But it - * does guarantee that some response has been received from the database, and in particular - * guarantees that if the request is invalid, an exception will be thrown by this method. - * - * @param statement the CQL query to execute (that can be any {@link Statement}). - * @return the result of the query. That result will never be null but can be empty (and will be - * for any non SELECT query). - * @throws NoHostAvailableException if no host in the cluster can be contacted successfully to - * execute this query. - * @throws QueryExecutionException if the query triggered an execution exception, i.e. an - * exception thrown by Cassandra when it cannot execute the query with the requested - * consistency level successfully. - * @throws QueryValidationException if the query if invalid (syntax error, unauthorized or any - * other validation problem). - * @throws UnsupportedFeatureException if the protocol version 1 is in use and a feature not - * supported has been used. Features that are not supported by the version protocol 1 include: - * BatchStatement, ResultSet paging and binary values in RegularStatement. - */ - ResultSet execute(Statement statement); - - /** - * Executes the provided query asynchronously. - * - *

This is a convenience method for {@code executeAsync(new SimpleStatement(query))}. - * - * @param query the CQL query to execute. - * @return a future on the result of the query. - */ - ResultSetFuture executeAsync(String query); - - /** - * Executes the provided query asynchronously using the provided values. - * - *

This is a convenience method for {@code executeAsync(new SimpleStatement(query, values))}. - * - * @param query the CQL query to execute. - * @param values values required for the execution of {@code query}. See {@link - * SimpleStatement#SimpleStatement(String, Object...)} for more details. - * @return a future on the result of the query. - * @throws UnsupportedFeatureException if version 1 of the protocol is in use (i.e. if you've - * forced version 1 through {@link Cluster.Builder#withProtocolVersion} or you use Cassandra - * 1.2). - */ - ResultSetFuture executeAsync(String query, Object... values); - - /** - * Executes the provided query asynchronously using the provided values. - * - *

This is a convenience method for {@code executeAsync(new SimpleStatement(query, values))}. - * - * @param query the CQL query to execute. - * @param values values required for the execution of {@code query}. See {@link - * SimpleStatement#SimpleStatement(String, Map)} for more details. - * @return a future on the result of the query. - * @throws UnsupportedFeatureException if version 1 or 2 of the protocol is in use (i.e. if you've - * forced it through {@link Cluster.Builder#withProtocolVersion} or you use Cassandra 1.2 or - * 2.0). - */ - ResultSetFuture executeAsync(String query, Map values); - - /** - * Executes the provided query asynchronously. - * - *

This method does not block. It returns as soon as the query has been passed to the - * underlying network stack. In particular, returning from this method does not guarantee that the - * query is valid or has even been submitted to a live node. Any exception pertaining to the - * failure of the query will be thrown when accessing the {@link ResultSetFuture}. - * - *

Note that for queries that don't return a result (INSERT, UPDATE and DELETE), you will need - * to access the ResultSetFuture (that is, call one of its {@code get} methods to make sure the - * query was successful. - * - * @param statement the CQL query to execute (that can be any {@code Statement}). - * @return a future on the result of the query. - * @throws UnsupportedFeatureException if the protocol version 1 is in use and a feature not - * supported has been used. Features that are not supported by the version protocol 1 include: - * BatchStatement, ResultSet paging and binary values in RegularStatement. - */ - ResultSetFuture executeAsync(Statement statement); - - /** - * Prepares the provided query string. - * - * @param query the CQL query string to prepare - * @return the prepared statement corresponding to {@code query}. - * @throws NoHostAvailableException if no host in the cluster can be contacted successfully to - * prepare this query. - */ - PreparedStatement prepare(String query); - - /** - * Prepares the provided query. - * - *

This method behaves like {@link #prepare(String)}, but the resulting {@code - * PreparedStatement} will inherit some of the properties set on {@code statement}: {@linkplain - * Statement#getRoutingKey(ProtocolVersion, CodecRegistry) routing key}, {@linkplain - * Statement#getConsistencyLevel() consistency level}, {@linkplain - * Statement#getSerialConsistencyLevel() serial consistency level}, {@linkplain - * Statement#isTracing() tracing flag}, {@linkplain Statement#getRetryPolicy() retry policy}, - * {@linkplain Statement#getOutgoingPayload() outgoing payload}, and {@linkplain - * Statement#isIdempotent() idempotence}. Concretely, this means that in the following code: - * - *

-   * RegularStatement toPrepare = new SimpleStatement("SELECT * FROM test WHERE k=?").setConsistencyLevel(ConsistencyLevel.QUORUM);
-   * PreparedStatement prepared = session.prepare(toPrepare);
-   * session.execute(prepared.bind("someValue"));
-   * 
- * - * the final execution will be performed with Quorum consistency. - * - *

Please note that if the same CQL statement is prepared more than once, all calls to this - * method will return the same {@code PreparedStatement} object but the method will still apply - * the properties of the prepared {@code Statement} to this object. - * - * @param statement the statement to prepare - * @return the prepared statement corresponding to {@code statement}. - * @throws NoHostAvailableException if no host in the cluster can be contacted successfully to - * prepare this statement. - * @throws IllegalArgumentException if {@code statement.getValues() != null} (values for executing - * a prepared statement should be provided after preparation though the {@link - * PreparedStatement#bind} method or through a corresponding {@link BoundStatement}). - */ - PreparedStatement prepare(RegularStatement statement); - - /** - * Prepares the provided query string asynchronously. - * - *

This method is equivalent to {@link #prepare(String)} except that it does not block but - * return a future instead. Any error during preparation will be thrown when accessing the future, - * not by this method itself. - * - * @param query the CQL query string to prepare - * @return a future on the prepared statement corresponding to {@code query}. - */ - ListenableFuture prepareAsync(String query); - - /** - * Prepares the provided query asynchronously. This method behaves like {@link - * #prepareAsync(String)}, but note that the resulting {@code PreparedStatement} will inherit the - * query properties set on {@code statement}. Concretely, this means that in the following code: - * - *

-   * RegularStatement toPrepare = new SimpleStatement("SELECT * FROM test WHERE k=?").setConsistencyLevel(ConsistencyLevel.QUORUM);
-   * PreparedStatement prepared = session.prepare(toPrepare);
-   * session.execute(prepared.bind("someValue"));
-   * 
- * - * the final execution will be performed with Quorum consistency. - * - *

Please note that if the same CQL statement is prepared more than once, all calls to this - * method will return the same {@code PreparedStatement} object but the method will still apply - * the properties of the prepared {@code Statement} to this object. - * - * @param statement the statement to prepare - * @return a future on the prepared statement corresponding to {@code statement}. - * @throws IllegalArgumentException if {@code statement.getValues() != null} (values for executing - * a prepared statement should be provided after preparation though the {@link - * PreparedStatement#bind} method or through a corresponding {@link BoundStatement}). - * @see Session#prepare(RegularStatement) - */ - ListenableFuture prepareAsync(RegularStatement statement); - - /** - * Initiates a shutdown of this session instance. - * - *

This method is asynchronous and return a future on the completion of the shutdown process. - * As soon as the session is shutdown, no new request will be accepted, but already submitted - * queries are allowed to complete. This method closes all connections of this session and - * reclaims all resources used by it. - * - *

If for some reason you wish to expedite this process, the {@link CloseFuture#force} can be - * called on the result future. - * - *

This method has no particular effect if the session was already closed (in which case the - * returned future will return immediately). - * - *

Note that this method does not close the corresponding {@code Cluster} instance (which holds - * additional resources, in particular internal executors that must be shut down in order for the - * client program to terminate). If you want to do so, use {@link Cluster#close}, but note that it - * will close all sessions created from that cluster. - * - * @return a future on the completion of the shutdown process. - */ - CloseFuture closeAsync(); - - /** - * Initiates a shutdown of this session instance and blocks until that shutdown completes. - * - *

This method is a shortcut for {@code closeAsync().get()}. - * - *

Note that this method does not close the corresponding {@code Cluster} instance (which holds - * additional resources, in particular internal executors that must be shut down in order for the - * client program to terminate). If you want to do so, use {@link Cluster#close}, but note that it - * will close all sessions created from that cluster. - */ - @Override - void close(); - - /** - * Whether this Session instance has been closed. - * - *

Note that this method returns true as soon as the closing of this Session has started but it - * does not guarantee that the closing is done. If you want to guarantee that the closing is done, - * you can call {@code close()} and wait until it returns (or call the get method on {@code - * closeAsync()} with a very short timeout and check this doesn't timeout). - * - * @return {@code true} if this Session instance has been closed, {@code false} otherwise. - */ - boolean isClosed(); - - /** - * Returns the {@code Cluster} object this session is part of. - * - * @return the {@code Cluster} object this session is part of. - */ - Cluster getCluster(); - - /** - * Return a snapshot of the state of this Session. - * - *

The returned object provides information on which hosts the session is connected to, how - * many connections are opened to each host, etc... The returned object is immutable, it is a - * snapshot of the Session State taken when this method is called. - * - * @return a snapshot of the state of this Session. - */ - State getState(); - - /** - * The state of a Session. - * - *

This mostly exposes information on the connections maintained by a Session: which host it is - * connected to, how many connections it has for each host, etc... - */ - interface State { - /** - * The Session to which this State corresponds to. - * - * @return the Session to which this State corresponds to. - */ - Session getSession(); - - /** - * The hosts to which the session is currently connected (more precisely, at the time this State - * has been grabbed). - * - *

Please note that this method really returns the hosts for which the session currently - * holds a connection pool. As such, it's unlikely but not impossible for a host to be listed in - * the output of this method but to have {@code getOpenConnections} return 0, if the pool itself - * is created but no connections have been successfully opened yet. - * - * @return an immutable collection of the hosts to which the session is connected. - */ - Collection getConnectedHosts(); - - /** - * The number of open connections to a given host. - * - *

Note that this refers to active connections. The actual number of connections - * also includes {@link #getTrashedConnections(Host)}. - * - * @param host the host to get open connections for. - * @return The number of open connections to {@code host}. If the session is not connected to - * that host, 0 is returned. - */ - int getOpenConnections(Host host); - - /** - * The number of "trashed" connections to a given host. - * - *

When the load to a host decreases, the driver will reclaim some connections in order to - * save resources. No requests are sent to these connections anymore, but they are kept open for - * an additional amount of time ({@link PoolingOptions#getIdleTimeoutSeconds()}), in case the - * load goes up again. This method counts connections in that state. - * - * @param host the host to get trashed connections for. - * @return The number of trashed connections to {@code host}. If the session is not connected to - * that host, 0 is returned. - */ - int getTrashedConnections(Host host); - - /** - * The number of queries that are currently being executed through a given host. - * - *

This corresponds to the number of queries that have been sent (by the session this is a - * State of) to the Cassandra Host on one of its connections but haven't yet returned. In that - * sense this provides a sort of measure of how busy the connections to that node are (at the - * time the {@code State} was grabbed at least). - * - * @param host the host to get in-flight queries for. - * @return the number of currently (as in 'at the time the state was grabbed') executing queries - * to {@code host}. - */ - int getInFlightQueries(Host host); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SessionManager.java b/driver-core/src/main/java/com/datastax/driver/core/SessionManager.java deleted file mode 100644 index c096d3232e5..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SessionManager.java +++ /dev/null @@ -1,859 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.Message.Response; -import com.datastax.driver.core.exceptions.DriverInternalError; -import com.datastax.driver.core.exceptions.InvalidQueryException; -import com.datastax.driver.core.exceptions.UnsupportedFeatureException; -import com.datastax.driver.core.exceptions.UnsupportedProtocolVersionException; -import com.datastax.driver.core.policies.LoadBalancingPolicy; -import com.datastax.driver.core.policies.ReconnectionPolicy; -import com.datastax.driver.core.policies.SpeculativeExecutionPolicy; -import com.datastax.driver.core.utils.MoreFutures; -import com.google.common.base.Functions; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import com.google.common.util.concurrent.AsyncFunction; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.SettableFuture; -import com.google.common.util.concurrent.Uninterruptibles; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Driver implementation of the Session interface. */ -class SessionManager extends AbstractSession { - - private static final Logger logger = LoggerFactory.getLogger(Session.class); - - final Cluster cluster; - final ConcurrentMap pools; - final HostConnectionPool.PoolState poolsState; - private final AtomicReference> initFuture = - new AtomicReference>(); - final AtomicReference closeFuture = new AtomicReference(); - - private volatile boolean isInit; - private volatile boolean isClosing; - - // Package protected, only Cluster should construct that. - SessionManager(Cluster cluster) { - this.cluster = cluster; - this.pools = new ConcurrentHashMap(); - this.poolsState = new HostConnectionPool.PoolState(); - } - - @Override - public Session init() { - try { - return Uninterruptibles.getUninterruptibly(initAsync()); - } catch (ExecutionException e) { - throw DriverThrowables.propagateCause(e); - } - } - - @Override - public ListenableFuture initAsync() { - // If we haven't initialized the cluster, do it now - cluster.init(); - - ListenableFuture existing = initFuture.get(); - if (existing != null) return existing; - - final SettableFuture myInitFuture = SettableFuture.create(); - if (!initFuture.compareAndSet(null, myInitFuture)) return initFuture.get(); - - Collection hosts = cluster.getMetadata().allHosts(); - ListenableFuture allPoolsCreatedFuture = createPools(hosts); - ListenableFuture allPoolsUpdatedFuture = - GuavaCompatibility.INSTANCE.transformAsync( - allPoolsCreatedFuture, - new AsyncFunction() { - @Override - @SuppressWarnings("unchecked") - public ListenableFuture apply(Object input) throws Exception { - isInit = true; - return (ListenableFuture) updateCreatedPools(); - } - }); - - GuavaCompatibility.INSTANCE.addCallback( - allPoolsUpdatedFuture, - new FutureCallback() { - @Override - public void onSuccess(Object result) { - myInitFuture.set(SessionManager.this); - } - - @Override - public void onFailure(Throwable t) { - SessionManager.this.closeAsync(); // don't leak the session - myInitFuture.setException(t); - } - }); - return myInitFuture; - } - - private ListenableFuture createPools(Collection hosts) { - List> futures = Lists.newArrayListWithCapacity(hosts.size()); - for (Host host : hosts) - if (host.state != Host.State.DOWN) futures.add(maybeAddPool(host, null)); - return Futures.allAsList(futures); - } - - @Override - public String getLoggedKeyspace() { - return poolsState.keyspace; - } - - @Override - public ResultSetFuture executeAsync(final Statement statement) { - if (isInit) { - DefaultResultSetFuture future = - new DefaultResultSetFuture( - this, cluster.manager.protocolVersion(), makeRequestMessage(statement, null)); - execute(future, statement); - return future; - } else { - // If the session is not initialized, we can't call makeRequestMessage() synchronously, - // because it - // requires internal Cluster state that might not be initialized yet (like the protocol - // version). - // Because of the way the future is built, we need another 'proxy' future that we can return - // now. - final ChainedResultSetFuture chainedFuture = new ChainedResultSetFuture(); - this.initAsync() - .addListener( - new Runnable() { - @Override - public void run() { - DefaultResultSetFuture actualFuture = - new DefaultResultSetFuture( - SessionManager.this, - cluster.manager.protocolVersion(), - makeRequestMessage(statement, null)); - execute(actualFuture, statement); - chainedFuture.setSource(actualFuture); - } - }, - executor()); - return chainedFuture; - } - } - - @Override - protected ListenableFuture prepareAsync( - String query, Map customPayload) { - Requests.Prepare request = new Requests.Prepare(query); - request.setCustomPayload(customPayload); - Connection.Future future = new Connection.Future(request); - execute(future, Statement.DEFAULT); - return toPreparedStatement(query, future); - } - - @Override - public CloseFuture closeAsync() { - CloseFuture future = closeFuture.get(); - if (future != null) return future; - - isClosing = true; - cluster.manager.removeSession(this); - - List futures = new ArrayList(pools.size()); - for (HostConnectionPool pool : pools.values()) futures.add(pool.closeAsync()); - - future = new CloseFuture.Forwarding(futures); - - return closeFuture.compareAndSet(null, future) - ? future - : closeFuture.get(); // We raced, it's ok, return the future that was actually set - } - - @Override - public boolean isClosed() { - return closeFuture.get() != null; - } - - @Override - public Cluster getCluster() { - return cluster; - } - - @Override - public Session.State getState() { - return new State(this); - } - - private ListenableFuture toPreparedStatement( - final String query, final Connection.Future future) { - return GuavaCompatibility.INSTANCE.transformAsync( - future, - new AsyncFunction() { - @Override - public ListenableFuture apply(Response response) { - switch (response.type) { - case RESULT: - Responses.Result rm = (Responses.Result) response; - switch (rm.kind) { - case PREPARED: - Responses.Result.Prepared pmsg = (Responses.Result.Prepared) rm; - PreparedStatement stmt = - DefaultPreparedStatement.fromMessage( - pmsg, cluster, query, poolsState.keyspace); - stmt = cluster.manager.addPrepared(stmt); - if (cluster.getConfiguration().getQueryOptions().isPrepareOnAllHosts()) { - // All Sessions are connected to the same nodes so it's enough to prepare only - // the nodes of this session. - // If that changes, we'll have to make sure this propagate to other sessions - // too. - return prepare(stmt, future.getEndPoint()); - } else { - return Futures.immediateFuture(stmt); - } - default: - return Futures.immediateFailedFuture( - new DriverInternalError( - String.format( - "%s response received when prepared statement was expected", - rm.kind))); - } - case ERROR: - return Futures.immediateFailedFuture( - ((Responses.Error) response).asException(future.getEndPoint())); - default: - return Futures.immediateFailedFuture( - new DriverInternalError( - String.format( - "%s response received when prepared statement was expected", - response.type))); - } - } - }, - executor()); - } - - Connection.Factory connectionFactory() { - return cluster.manager.connectionFactory; - } - - Configuration configuration() { - return cluster.manager.configuration; - } - - LoadBalancingPolicy loadBalancingPolicy() { - return cluster.manager.loadBalancingPolicy(); - } - - SpeculativeExecutionPolicy speculativeExecutionPolicy() { - return cluster.manager.speculativeExecutionPolicy(); - } - - ReconnectionPolicy reconnectionPolicy() { - return cluster.manager.reconnectionPolicy(); - } - - ListeningExecutorService executor() { - return cluster.manager.executor; - } - - ListeningExecutorService blockingExecutor() { - return cluster.manager.blockingExecutor; - } - - // Returns whether there was problem creating the pool - ListenableFuture forceRenewPool(final Host host, Connection reusedConnection) { - final HostDistance distance = cluster.manager.loadBalancingPolicy().distance(host); - if (distance == HostDistance.IGNORED) return Futures.immediateFuture(true); - - if (isClosing) return Futures.immediateFuture(false); - - final HostConnectionPool newPool = new HostConnectionPool(host, distance, this); - ListenableFuture poolInitFuture = newPool.initAsync(reusedConnection); - - final SettableFuture future = SettableFuture.create(); - - GuavaCompatibility.INSTANCE.addCallback( - poolInitFuture, - new FutureCallback() { - @Override - public void onSuccess(Void result) { - HostConnectionPool previous = pools.put(host, newPool); - if (previous == null) { - logger.debug("Added connection pool for {}", host); - } else { - logger.debug("Renewed connection pool for {}", host); - previous.closeAsync(); - } - - // If we raced with a session shutdown, ensure that the pool will be closed. - if (isClosing) { - newPool.closeAsync(); - pools.remove(host); - future.set(false); - } else { - future.set(true); - } - } - - @Override - public void onFailure(Throwable t) { - logger.warn("Error creating pool to " + host, t); - future.set(false); - } - }); - - return future; - } - - // Replace pool for a given host only if it's the given previous value (which can be null) - // This returns a future if the replacement was successful, or null if we raced. - private ListenableFuture replacePool( - final Host host, - HostDistance distance, - HostConnectionPool previous, - Connection reusedConnection) { - if (isClosing) return MoreFutures.VOID_SUCCESS; - - final HostConnectionPool newPool = new HostConnectionPool(host, distance, this); - if (previous == null) { - if (pools.putIfAbsent(host, newPool) != null) { - return null; - } - } else { - if (!pools.replace(host, previous, newPool)) { - return null; - } - if (!previous.isClosed()) { - logger.warn( - "Replacing a pool that wasn't closed. Closing it now, but this was not expected."); - previous.closeAsync(); - } - } - - ListenableFuture poolInitFuture = newPool.initAsync(reusedConnection); - - GuavaCompatibility.INSTANCE.addCallback( - poolInitFuture, - new FutureCallback() { - @Override - public void onSuccess(Void result) { - // If we raced with a session shutdown, ensure that the pool will be closed. - if (isClosing) { - newPool.closeAsync(); - pools.remove(host); - } - } - - @Override - public void onFailure(Throwable t) { - pools.remove(host); - } - }); - return poolInitFuture; - } - - // Returns whether there was problem creating the pool - ListenableFuture maybeAddPool(final Host host, Connection reusedConnection) { - final HostDistance distance = cluster.manager.loadBalancingPolicy().distance(host); - if (distance == HostDistance.IGNORED) return Futures.immediateFuture(true); - - HostConnectionPool previous = pools.get(host); - if (previous != null && !previous.isClosed()) return Futures.immediateFuture(true); - - while (true) { - previous = pools.get(host); - if (previous != null && !previous.isClosed()) return Futures.immediateFuture(true); - - final SettableFuture future = SettableFuture.create(); - ListenableFuture newPoolInit = replacePool(host, distance, previous, reusedConnection); - if (newPoolInit != null) { - GuavaCompatibility.INSTANCE.addCallback( - newPoolInit, - new FutureCallback() { - @Override - public void onSuccess(Void result) { - logger.debug("Added connection pool for {}", host); - future.set(true); - } - - @Override - public void onFailure(Throwable t) { - if (t instanceof UnsupportedProtocolVersionException) { - cluster.manager.logUnsupportedVersionProtocol( - host, ((UnsupportedProtocolVersionException) t).getUnsupportedVersion()); - cluster.manager.triggerOnDown(host, false); - } else if (t instanceof ClusterNameMismatchException) { - ClusterNameMismatchException e = (ClusterNameMismatchException) t; - cluster.manager.logClusterNameMismatch( - host, e.expectedClusterName, e.actualClusterName); - cluster.manager.triggerOnDown(host, false); - } else { - logger.warn("Error creating pool to " + host, t); - // do not mark the host down, as there could be other connections to it - // (e.g. the control connection, or another session pool). - // The conviction policy will mark it down if it has no more active connections. - } - // propagate errors; for all other exceptions, consider the pool init failed - // but allow the session init process to continue normally - if (t instanceof Error) future.setException(t); - else future.set(false); - } - }); - return future; - } - } - } - - CloseFuture removePool(Host host) { - final HostConnectionPool pool = pools.remove(host); - return pool == null ? CloseFuture.immediateFuture() : pool.closeAsync(); - } - - /* - * When the set of live nodes change, the loadbalancer will change his - * mind on host distances. It might change it on the node that came/left - * but also on other nodes (for instance, if a node dies, another - * previously ignored node may be now considered). - * - * This method ensures that all hosts for which a pool should exist - * have one, and hosts that shouldn't don't. - */ - ListenableFuture updateCreatedPools() { - // This method does nothing during initialization. Some hosts may be non-responsive but not yet - // marked DOWN; if - // we execute the code below we would try to create their pool over and over again. - // It's called explicitly at the end of init(), once isInit has been set to true. - if (!isInit) return MoreFutures.VOID_SUCCESS; - - // We do 2 iterations, so that we add missing pools first, and them remove all unecessary pool - // second. - // That way, we'll avoid situation where we'll temporarily lose connectivity - final List toRemove = new ArrayList(); - List> poolCreatedFutures = Lists.newArrayList(); - - for (Host h : cluster.getMetadata().allHosts()) { - HostDistance dist = loadBalancingPolicy().distance(h); - HostConnectionPool pool = pools.get(h); - - if (pool == null) { - if (dist != HostDistance.IGNORED && h.state == Host.State.UP) - poolCreatedFutures.add(maybeAddPool(h, null)); - } else if (dist != pool.hostDistance) { - if (dist == HostDistance.IGNORED) { - toRemove.add(h); - } else { - pool.hostDistance = dist; - pool.ensureCoreConnections(); - } - } - } - - // Wait pool creation before removing, so we don't lose connectivity - ListenableFuture allPoolsCreatedFuture = Futures.allAsList(poolCreatedFutures); - - return GuavaCompatibility.INSTANCE.transformAsync( - allPoolsCreatedFuture, - new AsyncFunction>() { - @Override - public ListenableFuture> apply(Object input) throws Exception { - List> poolRemovedFuture = - Lists.newArrayListWithCapacity(toRemove.size()); - for (Host h : toRemove) poolRemovedFuture.add(removePool(h)); - - return Futures.successfulAsList(poolRemovedFuture); - } - }); - } - - void updateCreatedPools(Host h) { - HostDistance dist = loadBalancingPolicy().distance(h); - HostConnectionPool pool = pools.get(h); - - try { - if (pool == null) { - if (dist != HostDistance.IGNORED && h.state == Host.State.UP) maybeAddPool(h, null).get(); - } else if (dist != pool.hostDistance) { - if (dist == HostDistance.IGNORED) { - removePool(h).get(); - } else { - pool.hostDistance = dist; - pool.ensureCoreConnections(); - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - logger.error("Unexpected error while refreshing connection pools", cause); - if (cause instanceof Error) throw ((Error) cause); - } - } - - void onDown(Host host) throws InterruptedException, ExecutionException { - // Note that with well behaved balancing policy (that ignore dead nodes), the removePool call is - // not necessary - // since updateCreatedPools should take care of it. But better protect against non well behaving - // policies. - removePool(host).force().get(); - updateCreatedPools().get(); - } - - void onRemove(Host host) throws InterruptedException, ExecutionException { - onDown(host); - } - - Message.Request makeRequestMessage(Statement statement, ByteBuffer pagingState) { - // We need the protocol version, which is only available once the cluster has initialized. - // Initialize the session to ensure this is the case. - // init() locks, so avoid if we know we don't need it. - if (!isInit) init(); - ProtocolVersion protocolVersion = cluster.manager.protocolVersion(); - CodecRegistry codecRegistry = cluster.manager.configuration.getCodecRegistry(); - - ConsistencyLevel consistency = statement.getConsistencyLevel(); - if (consistency == null) consistency = configuration().getQueryOptions().getConsistencyLevel(); - - ConsistencyLevel serialConsistency = statement.getSerialConsistencyLevel(); - if (protocolVersion.compareTo(ProtocolVersion.V3) < 0 && statement instanceof BatchStatement) { - if (serialConsistency != null) - throw new UnsupportedFeatureException( - protocolVersion, "Serial consistency on batch statements is not supported"); - } else if (serialConsistency == null) - serialConsistency = configuration().getQueryOptions().getSerialConsistencyLevel(); - - if (statement.getOutgoingPayload() != null && protocolVersion.compareTo(ProtocolVersion.V4) < 0) - throw new UnsupportedFeatureException( - protocolVersion, "Custom payloads are only supported since native protocol V4"); - - long defaultTimestamp = Long.MIN_VALUE; - if (protocolVersion.compareTo(ProtocolVersion.V3) >= 0) { - defaultTimestamp = statement.getDefaultTimestamp(); - if (defaultTimestamp == Long.MIN_VALUE) - defaultTimestamp = cluster.getConfiguration().getPolicies().getTimestampGenerator().next(); - } - - int fetchSize = statement.getFetchSize(); - ByteBuffer usedPagingState = pagingState; - - if (protocolVersion == ProtocolVersion.V1) { - assert pagingState == null; - // We don't let the user change the fetchSize globally if the proto v1 is used, so we just - // need to - // check for the case of a per-statement override - if (fetchSize <= 0) fetchSize = -1; - else if (fetchSize != Integer.MAX_VALUE) - throw new UnsupportedFeatureException(protocolVersion, "Paging is not supported"); - } else if (fetchSize <= 0) { - fetchSize = configuration().getQueryOptions().getFetchSize(); - } - - if (fetchSize == Integer.MAX_VALUE) fetchSize = -1; - - if (pagingState == null) { - usedPagingState = statement.getPagingState(); - } - - int nowInSeconds = statement.getNowInSeconds(); - if (nowInSeconds != Integer.MIN_VALUE && protocolVersion.compareTo(ProtocolVersion.V5) < 0) { - throw new UnsupportedFeatureException( - protocolVersion, "Now in seconds is only supported since native protocol V5"); - } - - if (statement instanceof StatementWrapper) - statement = ((StatementWrapper) statement).getWrappedStatement(); - - Message.Request request; - - if (statement instanceof RegularStatement) { - RegularStatement rs = (RegularStatement) statement; - - // It saddens me that we special case for the query builder here, but for now this is simpler. - // We could provide a general API in RegularStatement instead at some point but it's unclear - // what's - // the cleanest way to do that is right now (and it's probably not really that useful anyway). - if (protocolVersion == ProtocolVersion.V1 - && rs instanceof com.datastax.driver.core.querybuilder.BuiltStatement) - ((com.datastax.driver.core.querybuilder.BuiltStatement) rs).setForceNoValues(true); - - ByteBuffer[] rawPositionalValues = rs.getValues(protocolVersion, codecRegistry); - Map rawNamedValues = rs.getNamedValues(protocolVersion, codecRegistry); - - if (protocolVersion == ProtocolVersion.V1 - && (rawPositionalValues != null || rawNamedValues != null)) - throw new UnsupportedFeatureException(protocolVersion, "Binary values are not supported"); - - if (protocolVersion == ProtocolVersion.V2 && rawNamedValues != null) - throw new UnsupportedFeatureException(protocolVersion, "Named values are not supported"); - - ByteBuffer[] positionalValues = - rawPositionalValues == null ? Requests.EMPTY_BB_ARRAY : rawPositionalValues; - Map namedValues = - rawNamedValues == null ? Collections.emptyMap() : rawNamedValues; - - String qString = rs.getQueryString(codecRegistry); - - Requests.QueryProtocolOptions options = - new Requests.QueryProtocolOptions( - Message.Request.Type.QUERY, - consistency, - positionalValues, - namedValues, - false, - fetchSize, - usedPagingState, - serialConsistency, - defaultTimestamp, - nowInSeconds); - request = new Requests.Query(qString, options, statement.isTracing()); - } else if (statement instanceof BoundStatement) { - BoundStatement bs = (BoundStatement) statement; - if (!cluster.manager.preparedQueries.containsKey( - bs.statement.getPreparedId().boundValuesMetadata.id)) { - throw new InvalidQueryException( - String.format( - "Tried to execute unknown prepared query : %s. " - + "You may have used a PreparedStatement that was created with another Cluster instance.", - bs.statement.getPreparedId().boundValuesMetadata.id)); - } - if (protocolVersion.compareTo(ProtocolVersion.V4) < 0) bs.ensureAllSet(); - - // skip resultset metadata if version > 1 (otherwise this feature is not supported) - // and if we already have metadata for the prepared statement being executed. - boolean skipMetadata = - protocolVersion != ProtocolVersion.V1 - && bs.statement.getPreparedId().resultSetMetadata.variables != null; - Requests.QueryProtocolOptions options = - new Requests.QueryProtocolOptions( - Message.Request.Type.EXECUTE, - consistency, - bs.wrapper.values, - Collections.emptyMap(), - skipMetadata, - fetchSize, - usedPagingState, - serialConsistency, - defaultTimestamp, - nowInSeconds); - request = - new Requests.Execute( - bs.statement.getPreparedId().boundValuesMetadata.id, - bs.statement.getPreparedId().resultSetMetadata.id, - options, - statement.isTracing()); - } else { - assert statement instanceof BatchStatement : statement; - assert pagingState == null; - - if (protocolVersion == ProtocolVersion.V1) - throw new UnsupportedFeatureException( - protocolVersion, "Protocol level batching is not supported"); - - BatchStatement bs = (BatchStatement) statement; - if (protocolVersion.compareTo(ProtocolVersion.V4) < 0) bs.ensureAllSet(); - BatchStatement.IdAndValues idAndVals = bs.getIdAndValues(protocolVersion, codecRegistry); - Requests.BatchProtocolOptions options = - new Requests.BatchProtocolOptions( - consistency, serialConsistency, defaultTimestamp, nowInSeconds); - request = - new Requests.Batch( - bs.batchType, idAndVals.ids, idAndVals.values, options, statement.isTracing()); - } - - request.setCustomPayload(statement.getOutgoingPayload()); - return request; - } - - /** - * Execute the provided request. - * - *

This method will find a suitable node to connect to using the {@link LoadBalancingPolicy} - * and handle host failover. - */ - void execute(final RequestHandler.Callback callback, final Statement statement) { - if (this.isClosed()) { - callback.onException( - null, new IllegalStateException("Could not send request, session is closed"), 0, 0); - return; - } - if (isInit) new RequestHandler(this, callback, statement).sendRequest(); - else - this.initAsync() - .addListener( - new Runnable() { - @Override - public void run() { - new RequestHandler(SessionManager.this, callback, statement).sendRequest(); - } - }, - executor()); - } - - private ListenableFuture prepare( - final PreparedStatement statement, EndPoint toExclude) { - final String query = statement.getQueryString(); - List> futures = Lists.newArrayListWithExpectedSize(pools.size()); - for (final Map.Entry entry : pools.entrySet()) { - if (entry.getKey().getEndPoint().equals(toExclude)) continue; - - try { - // Preparing is not critical: if it fails, it will fix itself later when the user tries to - // execute - // the prepared query. So don't wait if no connection is available, simply abort. - ListenableFuture connectionFuture = - entry.getValue().borrowConnection(0, TimeUnit.MILLISECONDS, 0); - ListenableFuture prepareFuture = - GuavaCompatibility.INSTANCE.transformAsync( - connectionFuture, - new AsyncFunction() { - @Override - public ListenableFuture apply(final Connection c) throws Exception { - Connection.Future responseFuture = c.write(new Requests.Prepare(query)); - GuavaCompatibility.INSTANCE.addCallback( - responseFuture, - new FutureCallback() { - @Override - public void onSuccess(Response result) { - c.release(); - } - - @Override - public void onFailure(Throwable t) { - logger.debug( - String.format( - "Unexpected error while preparing query (%s) on %s", - query, entry.getKey()), - t); - c.release(); - } - }); - return responseFuture; - } - }); - futures.add(prepareFuture); - } catch (Exception e) { - // Again, not being able to prepare the query right now is no big deal, so just ignore - } - } - // Return the statement when all futures are done - return GuavaCompatibility.INSTANCE.transform( - Futures.successfulAsList(futures), Functions.constant(statement)); - } - - ResultSetFuture executeQuery(Message.Request msg, Statement statement) { - DefaultResultSetFuture future = - new DefaultResultSetFuture( - this, configuration().getProtocolOptions().getProtocolVersion(), msg); - execute(future, statement); - return future; - } - - void cleanupIdleConnections(long now) { - for (HostConnectionPool pool : pools.values()) { - pool.cleanupIdleConnections(now); - } - } - - private static class State implements Session.State { - - private final SessionManager session; - private final List connectedHosts; - private final int[] openConnections; - private final int[] trashedConnections; - private final int[] inFlightQueries; - - private State(SessionManager session) { - this.session = session; - this.connectedHosts = ImmutableList.copyOf(session.pools.keySet()); - - this.openConnections = new int[connectedHosts.size()]; - this.trashedConnections = new int[connectedHosts.size()]; - this.inFlightQueries = new int[connectedHosts.size()]; - - int i = 0; - for (Host h : connectedHosts) { - HostConnectionPool p = session.pools.get(h); - // It's possible we race and the host has been removed since the beginning of this - // functions. In that case, the fact it's part of getConnectedHosts() but has no opened - // connections will be slightly weird, but it's unlikely enough that we don't bother - // avoiding. - if (p == null) { - openConnections[i] = 0; - trashedConnections[i] = 0; - inFlightQueries[i] = 0; - continue; - } - - openConnections[i] = p.opened(); - inFlightQueries[i] = p.totalInFlight.get(); - trashedConnections[i] = p.trashed(); - i++; - } - } - - private int getIdx(Host h) { - // We guarantee that we only ever create one Host object per-address, which means that '==' - // comparison is a proper way to test Host equality. Given that, the number of hosts - // per-session will always be small enough (even 1000 is kind of small and even with a 1000+ - // node cluster, you probably don't want a Session to connect to all of them) that iterating - // over connectedHosts will never be much more inefficient than keeping a - // Map. And it's less garbage/memory consumption so... - for (int i = 0; i < connectedHosts.size(); i++) if (h == connectedHosts.get(i)) return i; - return -1; - } - - @Override - public Session getSession() { - return session; - } - - @Override - public Collection getConnectedHosts() { - return connectedHosts; - } - - @Override - public int getOpenConnections(Host host) { - int i = getIdx(host); - return i < 0 ? 0 : openConnections[i]; - } - - @Override - public int getTrashedConnections(Host host) { - int i = getIdx(host); - return i < 0 ? 0 : trashedConnections[i]; - } - - @Override - public int getInFlightQueries(Host host) { - int i = getIdx(host); - return i < 0 ? 0 : inFlightQueries[i]; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SettableByIndexData.java b/driver-core/src/main/java/com/datastax/driver/core/SettableByIndexData.java deleted file mode 100644 index aba3af457da..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SettableByIndexData.java +++ /dev/null @@ -1,580 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.CodecNotFoundException; -import com.datastax.driver.core.exceptions.InvalidTypeException; -import com.google.common.reflect.TypeToken; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -/** Collection of (typed) CQL values that can be set by index (starting at zero). */ -public interface SettableByIndexData> { - - /** - * Sets the {@code i}th value to the provided boolean. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code boolean}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(int)} or {@code - * set(i, v, Boolean.class)} - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setBool(int i, boolean v); - - /** - * Set the {@code i}th value to the provided byte. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code tinyint}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(int)} or {@code - * set(i, v, Byte.class)} - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setByte(int i, byte v); - - /** - * Set the {@code i}th value to the provided short. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code smallint}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(int)} or {@code - * set(i, v, Short.class)} - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setShort(int i, short v); - - /** - * Set the {@code i}th value to the provided integer. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code int}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(int)} or {@code - * set(i, v, Integer.class)} - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setInt(int i, int v); - - /** - * Sets the {@code i}th value to the provided long. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code bigint}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(int)} or {@code - * set(i, v, Long.class)} - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setLong(int i, long v); - - /** - * Set the {@code i}th value to the provided date. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code timestamp}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setTimestamp(int i, Date v); - - /** - * Set the {@code i}th value to the provided date (without time). - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code date}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setDate(int i, LocalDate v); - - /** - * Set the {@code i}th value to the provided time as a long in nanoseconds since midnight. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code time}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setTime(int i, long v); - - /** - * Sets the {@code i}th value to the provided float. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code float}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(int)} or {@code - * set(i, v, Float.class)} - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setFloat(int i, float v); - - /** - * Sets the {@code i}th value to the provided double. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code double}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(int)} or {@code - * set(i, v, Double.class)}. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setDouble(int i, double v); - - /** - * Sets the {@code i}th value to the provided string. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL types {@code text}, {@code varchar} and {@code ascii}, this will - * be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setString(int i, String v); - - /** - * Sets the {@code i}th value to the provided byte buffer. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code blob}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setBytes(int i, ByteBuffer v); - - /** - * Sets the {@code i}th value to the provided byte buffer. - * - *

This method does not use any codec; it sets the value in its binary form directly. If you - * insert data that is not compatible with the underlying CQL type, you will get an {@code - * InvalidQueryException} at execute time. - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - */ - public T setBytesUnsafe(int i, ByteBuffer v); - - /** - * Sets the {@code i}th value to the provided big integer. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code varint}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setVarint(int i, BigInteger v); - - /** - * Sets the {@code i}th value to the provided big decimal. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code decimal}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setDecimal(int i, BigDecimal v); - - /** - * Sets the {@code i}th value to the provided UUID. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL types {@code uuid} and {@code timeuuid}, this will be the built-in - * codec). - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setUUID(int i, UUID v); - - /** - * Sets the {@code i}th value to the provided inet address. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code inet}, this will be the built-in codec). - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setInet(int i, InetAddress v); - - /** - * Sets the {@code i}th value to the provided list. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (the type of the elements in the Java list is not considered). If two or - * more codecs target that CQL type, the one that was first registered will be used. For this - * reason, it is generally preferable to use the more deterministic methods {@link #setList(int, - * List, Class)} or {@link #setList(int, List, TypeToken)}. - * - * @param i the index of the value to set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setList(int i, List v); - - /** - * Sets the {@code i}th value to the provided list, which elements are of the provided Java class. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of lists - * of the given Java type to the underlying CQL type. - * - *

If the type of the elements is generic, use {@link #setList(int, List, TypeToken)}. - * - * @param i the index of the value to set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param elementsClass the class for the elements of the list. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setList(int i, List v, Class elementsClass); - - /** - * Sets the {@code i}th value to the provided list, which elements are of the provided Java type. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of lists - * of the given Java type to the underlying CQL type. - * - * @param i the index of the value to set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param elementsType the type for the elements of the list. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setList(int i, List v, TypeToken elementsType); - - /** - * Sets the {@code i}th value to the provided map. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (the type of the elements in the Java map is not considered). If two or - * more codecs target that CQL type, the one that was first registered will be used. For this - * reason, it is generally preferable to use the more deterministic methods {@link #setMap(int, - * Map, Class, Class)} or {@link #setMap(int, Map, TypeToken, TypeToken)}. - * - * @param i the index of the value to set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setMap(int i, Map v); - - /** - * Sets the {@code i}th value to the provided map, which keys and values are of the provided Java - * classes. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of lists - * of the given Java types to the underlying CQL type. - * - *

If the type of the keys or values is generic, use {@link #setMap(int, Map, TypeToken, - * TypeToken)}. - * - * @param i the index of the value to set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param keysClass the class for the keys of the map. - * @param valuesClass the class for the values of the map. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setMap(int i, Map v, Class keysClass, Class valuesClass); - - /** - * Sets the {@code i}th value to the provided map, which keys and values are of the provided Java - * types. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of lists - * of the given Java types to the underlying CQL type. - * - * @param i the index of the value to set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param keysType the type for the keys of the map. - * @param valuesType the type for the values of the map. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setMap(int i, Map v, TypeToken keysType, TypeToken valuesType); - - /** - * Sets the {@code i}th value to the provided set. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (the type of the elements in the Java set is not considered). If two or - * more codecs target that CQL type, the one that was first registered will be used. For this - * reason, it is generally preferable to use the more deterministic methods {@link #setSet(int, - * Set, Class)} or {@link #setSet(int, Set, TypeToken)}. - * - * @param i the index of the value to set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setSet(int i, Set v); - - /** - * Sets the {@code i}th value to the provided set, which elements are of the provided Java class. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of sets - * of the given Java type to the underlying CQL type. - * - *

If the type of the elements is generic, use {@link #setSet(int, Set, TypeToken)}. - * - * @param i the index of the value to set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param elementsClass the class for the elements of the set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setSet(int i, Set v, Class elementsClass); - - /** - * Sets the {@code i}th value to the provided set, which elements are of the provided Java type. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of sets - * of the given Java type to the underlying CQL type. - * - * @param i the index of the value to set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param elementsType the type for the elements of the set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setSet(int i, Set v, TypeToken elementsType); - - /** - * Sets the {@code i}th value to the provided UDT value. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of - * {@code UDTValue} to the underlying CQL type. - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setUDTValue(int i, UDTValue v); - - /** - * Sets the {@code i}th value to the provided tuple value. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of - * {@code TupleValue} to the underlying CQL type. - * - * @param i the index of the value to set. - * @param v the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setTupleValue(int i, TupleValue v); - - /** - * Sets the {@code i}th value to {@code null}. - * - *

This is mainly intended for CQL types which map to native Java types. - * - * @param i the index of the value to set. - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - */ - public T setToNull(int i); - - /** - * Sets the {@code i}th value to the provided value of the provided Java class. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of the - * provided Java class to the underlying CQL type. - * - *

If the Java type is generic, use {@link #set(int, Object, TypeToken)} instead. - * - * @param i the index of the value to set. - * @param v the value to set; may be {@code null}. - * @param targetClass The Java class to convert to; must not be {@code null}; - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - T set(int i, V v, Class targetClass); - - /** - * Sets the {@code i}th value to the provided value of the provided Java type. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of the - * provided Java type to the underlying CQL type. - * - * @param i the index of the value to set. - * @param v the value to set; may be {@code null}. - * @param targetType The Java type to convert to; must not be {@code null}; - * @return this object. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - T set(int i, V v, TypeToken targetType); - - /** - * Sets the {@code i}th value to the provided value, converted using the given {@link TypeCodec}. - * - *

This method entirely bypasses the {@link CodecRegistry} and forces the driver to use the - * given codec instead. This can be useful if the codec would collide with a previously registered - * one, or if you want to use the codec just once without registering it. - * - *

It is the caller's responsibility to ensure that the given codec {@link - * TypeCodec#accepts(DataType) accepts} the underlying CQL type; failing to do so may result in - * {@link InvalidTypeException}s being thrown. - * - * @param i the index of the value to set. - * @param v the value to set; may be {@code null}. - * @param codec The {@link TypeCodec} to use to serialize the value; may not be {@code null}. - * @return this object. - * @throws InvalidTypeException if the given codec does not {@link TypeCodec#accepts(DataType) - * accept} the underlying CQL type. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - */ - T set(int i, V v, TypeCodec codec); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SettableByNameData.java b/driver-core/src/main/java/com/datastax/driver/core/SettableByNameData.java deleted file mode 100644 index 4c606702cf5..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SettableByNameData.java +++ /dev/null @@ -1,617 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.CodecNotFoundException; -import com.datastax.driver.core.exceptions.InvalidTypeException; -import com.google.common.reflect.TypeToken; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.nio.ByteBuffer; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -/** Collection of (typed) CQL values that can set by name. */ -public interface SettableByNameData> { - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided boolean. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code boolean}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(String)} or {@code - * set(name, v, Boolean.class)}. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setBool(String name, boolean v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided byte. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code tinyint}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(String)} or {@code - * set(name, v, Byte.class)}. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setByte(String name, byte v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided short. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code smallint}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(String)} or {@code - * set(name, v, Short.class)}. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setShort(String name, short v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided integer. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code int}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(String)} or {@code - * set(name, v, Integer.class)}. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setInt(String name, int v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided long. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code bigint}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(String)} or {@code - * set(name, v, Long.class)}. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setLong(String name, long v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided date. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code timestamp}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setTimestamp(String name, Date v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided date (without - * time). - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code date}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setDate(String name, LocalDate v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided time as a long in - * nanoseconds since midnight. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code time}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setTime(String name, long v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided float. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code float}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(String)} or {@code - * set(name, v, Float.class)}. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setFloat(String name, float v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided double. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code double}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. To set the value to NULL, use {@link #setToNull(String)} or {@code - * set(name, v, Double.class)}. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setDouble(String name, double v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided string. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL types {@code text}, {@code varchar} and {@code ascii}, this will - * be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setString(String name, String v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided byte buffer. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code blob}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setBytes(String name, ByteBuffer v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided byte buffer. - * - *

This method does not use any codec; it sets the value in its binary form directly. If you - * insert data that is not compatible with the underlying CQL type, you will get an {@code - * InvalidQueryException} at execute time. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - */ - public T setBytesUnsafe(String name, ByteBuffer v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided big integer. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code varint}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setVarint(String name, BigInteger v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided big decimal. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code decimal}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setDecimal(String name, BigDecimal v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided UUID. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL types {@code uuid} and {@code timeuuid}, this will be the built-in - * codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setUUID(String name, UUID v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided inet address. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (for CQL type {@code inet}, this will be the built-in codec). - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setInet(String name, InetAddress v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided list. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (the type of the elements in the Java list is not considered). If two or - * more codecs target that CQL type, the one that was first registered will be used. For this - * reason, it is generally preferable to use the more deterministic methods {@link - * #setList(String, List, Class)} or {@link #setList(String, List, TypeToken)}. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setList(String name, List v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided list, which - * elements are of the provided Java class. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of lists - * of the given Java type to the underlying CQL type. - * - *

If the type of the elements is generic, use {@link #setList(String, List, TypeToken)}. - * - * @param name the name of the value to set; if {@code name} is present multiple - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param elementsClass the class for the elements of the list. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setList(String name, List v, Class elementsClass); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided list, which - * elements are of the provided Java type. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of lists - * of the given Java type to the underlying CQL type. - * - * @param name the name of the value to set; if {@code name} is present multiple - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param elementsType the type for the elements of the list. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setList(String name, List v, TypeToken elementsType); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided map. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (the type of the elements in the Java map is not considered). If two or - * more codecs target that CQL type, the one that was first registered will be used. For this - * reason, it is generally preferable to use the more deterministic methods {@link #setMap(String, - * Map, Class, Class)} or {@link #setMap(String, Map, TypeToken, TypeToken)}. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setMap(String name, Map v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided map, which keys - * and values are of the provided Java classes. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of lists - * of the given Java types to the underlying CQL type. - * - *

If the type of the keys or values is generic, use {@link #setMap(String, Map, TypeToken, - * TypeToken)}. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param keysClass the class for the keys of the map. - * @param valuesClass the class for the values of the map. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setMap(String name, Map v, Class keysClass, Class valuesClass); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided map, which keys - * and values are of the provided Java types. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of lists - * of the given Java types to the underlying CQL type. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param keysType the type for the keys of the map. - * @param valuesType the type for the values of the map. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setMap(String name, Map v, TypeToken keysType, TypeToken valuesType); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided set. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion to the - * underlying CQL type (the type of the elements in the Java set is not considered). If two or - * more codecs target that CQL type, the one that was first registered will be used. For this - * reason, it is generally preferable to use the more deterministic methods {@link #setSet(String, - * Set, Class)} or {@link #setSet(String, Set, TypeToken)}. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setSet(String name, Set v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided set, which - * elements are of the provided Java class. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of sets - * of the given Java type to the underlying CQL type. - * - *

If the type of the elements is generic, use {@link #setSet(String, Set, TypeToken)}. - * - * @param name the name of the value to set; if {@code name} is present multiple - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param elementsClass the class for the elements of the set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setSet(String name, Set v, Class elementsClass); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided set, which - * elements are of the provided Java type. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of sets - * of the given Java type to the underlying CQL type. - * - * @param name the name of the value to set; if {@code name} is present multiple - * @param v the value to set. Note that {@code null} values inside collections are not supported - * by CQL. - * @param elementsType the type for the elements of the set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws NullPointerException if {@code v} contains null values. Nulls are not supported in - * collections by CQL. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setSet(String name, Set v, TypeToken elementsType); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided UDT value. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of - * {@code UDTValue} to the underlying CQL type. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setUDTValue(String name, UDTValue v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided tuple value. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of - * {@code TupleValue} to the underlying CQL type. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - public T setTupleValue(String name, TupleValue v); - - /** - * Sets the value for (all occurrences of) variable {@code name} to {@code null}. - * - *

This is mainly intended for CQL types which map to native Java types. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - */ - public T setToNull(String name); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided value of the - * provided Java class. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of the - * provided Java class to the underlying CQL type. - * - *

If the Java type is generic, use {@link #set(String, Object, TypeToken)} instead. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set; may be {@code null}. - * @param targetClass The Java class to convert to; must not be {@code null}; - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - T set(String name, V v, Class targetClass); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided value of the - * provided Java type. - * - *

This method uses the {@link CodecRegistry} to find a codec to handle the conversion of the - * provided Java type to the underlying CQL type. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set; may be {@code null}. - * @param targetType The Java type to convert to; must not be {@code null}; - * @return this object. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - * @throws CodecNotFoundException if there is no registered codec to convert the value to the - * underlying CQL type. - */ - T set(String name, V v, TypeToken targetType); - - /** - * Sets the value for (all occurrences of) variable {@code name} to the provided value, converted - * using the given {@link TypeCodec}. - * - *

This method entirely bypasses the {@link CodecRegistry} and forces the driver to use the - * given codec instead. This can be useful if the codec would collide with a previously registered - * one, or if you want to use the codec just once without registering it. - * - *

It is the caller's responsibility to ensure that the given codec {@link - * TypeCodec#accepts(DataType) accepts} the underlying CQL type; failing to do so may result in - * {@link InvalidTypeException}s being thrown. - * - * @param name the name of the value to set; if {@code name} is present multiple times, all its - * values are set. - * @param v the value to set; may be {@code null}. - * @param codec The {@link TypeCodec} to use to serialize the value; may not be {@code null}. - * @return this object. - * @throws InvalidTypeException if the given codec does not {@link TypeCodec#accepts(DataType) - * accept} the underlying CQL type. - * @throws IllegalArgumentException if {@code name} is not a valid name for this object. - */ - T set(String name, V v, TypeCodec codec); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SettableData.java b/driver-core/src/main/java/com/datastax/driver/core/SettableData.java deleted file mode 100644 index 493c487670d..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SettableData.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Collection of (typed) CQL values that can be set either by index (starting at zero) or by name. - */ -public interface SettableData> - extends SettableByIndexData, SettableByNameData {} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SimpleJSONParser.java b/driver-core/src/main/java/com/datastax/driver/core/SimpleJSONParser.java deleted file mode 100644 index b8c38db510d..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SimpleJSONParser.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * A very simple json parser. The only reason we need to read json in the driver is because for - * historical reason Cassandra encodes a few properties using json in the schema and we need to - * decode them. - * - *

We however don't need a full-blown JSON library because: 1) we know we only need to decode - * string lists and string maps 2) we can basically assume the input is valid, we don't particularly - * have to bother about decoding exactly JSON as long as we at least decode what we need. 3) we - * don't really care much about performance, none of this is done in performance sensitive parts. - * - *

So instead of pulling a new dependency, we roll out our own very dumb parser. We should - * obviously not expose this publicly. - */ -class SimpleJSONParser { - - private final String input; - private int idx; - - private SimpleJSONParser(String input) { - this.input = input; - } - - public static List parseStringList(String input) { - if (input == null || input.isEmpty()) return Collections.emptyList(); - - List output = new ArrayList(); - SimpleJSONParser parser = new SimpleJSONParser(input); - if (parser.nextCharSkipSpaces() != '[') - throw new IllegalArgumentException("Not a JSON list: " + input); - - char c = parser.nextCharSkipSpaces(); - if (c == ']') return output; - - while (true) { - assert c == '"'; - output.add(parser.nextString()); - c = parser.nextCharSkipSpaces(); - if (c == ']') return output; - assert c == ','; - c = parser.nextCharSkipSpaces(); - } - } - - public static Map parseStringMap(String input) { - if (input == null || input.isEmpty()) return Collections.emptyMap(); - - Map output = new HashMap(); - SimpleJSONParser parser = new SimpleJSONParser(input); - if (parser.nextCharSkipSpaces() != '{') - throw new IllegalArgumentException("Not a JSON map: " + input); - - char c = parser.nextCharSkipSpaces(); - if (c == '}') return output; - - while (true) { - assert c == '"'; - String key = parser.nextString(); - c = parser.nextCharSkipSpaces(); - assert c == ':'; - c = parser.nextCharSkipSpaces(); - assert c == '"'; - String value = parser.nextString(); - output.put(key, value); - c = parser.nextCharSkipSpaces(); - if (c == '}') return output; - assert c == ','; - c = parser.nextCharSkipSpaces(); - } - } - - /** Read the next char, the one at position idx, and advance ix. */ - private char nextChar() { - if (idx >= input.length()) throw new IllegalArgumentException("Invalid json input: " + input); - return input.charAt(idx++); - } - - /** Same as nextChar, except that it skips space characters (' ', '\t' and '\n'). */ - private char nextCharSkipSpaces() { - char c = nextChar(); - while (c == ' ' || c == '\t' || c == '\n') c = nextChar(); - return c; - } - - /** - * Reads a String, assuming idx is on the first character of the string (i.e. the one after the - * opening double-quote character). After the string has been read, idx will be on the first - * character after the closing double-quote. - */ - private String nextString() { - assert input.charAt(idx - 1) == '"' : "Char is '" + input.charAt(idx - 1) + '\''; - StringBuilder sb = new StringBuilder(); - while (true) { - char c = nextChar(); - switch (c) { - case '\n': - case '\r': - throw new IllegalArgumentException("Unterminated string"); - case '\\': - c = nextChar(); - switch (c) { - case 'b': - sb.append('\b'); - break; - case 't': - sb.append('\t'); - break; - case 'n': - sb.append('\n'); - break; - case 'f': - sb.append('\f'); - break; - case 'r': - sb.append('\r'); - break; - case 'u': - sb.append((char) Integer.parseInt(input.substring(idx, idx + 4), 16)); - idx += 4; - break; - case '"': - case '\'': - case '\\': - case '/': - sb.append(c); - break; - default: - throw new IllegalArgumentException("Illegal escape"); - } - break; - default: - if (c == '"') return sb.toString(); - sb.append(c); - } - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SimpleStatement.java b/driver-core/src/main/java/com/datastax/driver/core/SimpleStatement.java deleted file mode 100644 index 2a1be224ed3..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SimpleStatement.java +++ /dev/null @@ -1,410 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.InvalidTypeException; -import java.nio.ByteBuffer; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** A simple {@code RegularStatement} implementation built directly from a query string. */ -public class SimpleStatement extends RegularStatement { - - private final String query; - private final Object[] values; - private final Map namedValues; - - private volatile ByteBuffer routingKey; - private volatile String keyspace; - - /** - * Creates a new {@code SimpleStatement} with the provided query string (and no values). - * - * @param query the query string. - */ - public SimpleStatement(String query) { - this(query, (Object[]) null); - } - - /** - * Creates a new {@code SimpleStatement} with the provided query string and values. - * - *

This version of SimpleStatement is useful when you want to execute a query only once (and - * thus do not want to resort to prepared statement), but do not want to convert all column values - * to string (typically, if you have blob values, encoding them to a hexadecimal string is not - * very efficient). In that case, you can provide a query string with bind markers to this - * constructor along with the values for those bind variables. When executed, the server will - * prepare the provided, bind the provided values to that prepare statement and execute the - * resulting statement. Thus, - * - *

-   *   session.execute(new SimpleStatement(query, value1, value2, value3));
-   * 
- * - * is functionally equivalent to - * - *
-   *   PreparedStatement ps = session.prepare(query);
-   *   session.execute(ps.bind(value1, value2, value3));
-   * 
- * - * except that the former version: - * - *
    - *
  • Requires only one round-trip to a Cassandra node. - *
  • Does not left any prepared statement stored in memory (neither client or server side) - * once it has been executed. - *
- * - *

Note that the types of the {@code values} provided to this method will not be validated by - * the driver as is done by {@link BoundStatement#bind} since {@code query} is not parsed (and - * hence the driver cannot know what those values should be). The codec to serialize each value - * will be chosen in the codec registry associated with the cluster executing this statement, - * based on the value's Java type (this is the equivalent to calling {@link - * CodecRegistry#codecFor(Object)}). If too many or too few values are provided, or if a value is - * not a valid one for the variable it is bound to, an {@link - * com.datastax.driver.core.exceptions.InvalidQueryException} will be thrown by Cassandra at - * execution time. A {@code CodecNotFoundException} may be thrown by this constructor however, if - * the codec registry does not know how to handle one of the values. - * - *

If you have a single value of type {@code Map}, you can't call this - * constructor using the varargs syntax, because the signature collides with {@link - * #SimpleStatement(String, Map)}. To prevent this, pass an explicit array object: - * - *

-   * new SimpleStatement("...", new Object[]{m});
-   * 
- * - * @param query the query string. - * @param values values required for the execution of {@code query}. - * @throws IllegalArgumentException if the number of values is greater than 65535. - */ - public SimpleStatement(String query, Object... values) { - if (values != null && values.length > 65535) - throw new IllegalArgumentException("Too many values, the maximum allowed is 65535"); - this.query = query; - this.values = values; - this.namedValues = null; - } - - /** - * Creates a new {@code SimpleStatement} with the provided query string and named values. - * - *

This constructor requires that the query string use named placeholders, for example: - * - *

{@code
-   * new SimpleStatement("SELECT * FROM users WHERE id = :i", ImmutableMap.of("i", 1));
-   * }
- * - * Make sure that the map is correctly typed {@code Map}, otherwise you might - * accidentally call {@link #SimpleStatement(String, Object...)} with a positional value of type - * map. - * - *

The types of the values will be handled the same way as with anonymous placeholders (see - * {@link #SimpleStatement(String, Object...)}). - * - *

Simple statements with named values are only supported starting with native protocol {@link - * ProtocolVersion#V3 v3}. With earlier versions, an {@link - * com.datastax.driver.core.exceptions.UnsupportedFeatureException} will be thrown at execution - * time. - * - * @param query the query string. - * @param values named values required for the execution of {@code query}. - * @throws IllegalArgumentException if the number of values is greater than 65535. - */ - public SimpleStatement(String query, Map values) { - if (values.size() > 65535) - throw new IllegalArgumentException("Too many values, the maximum allowed is 65535"); - this.query = query; - this.values = null; - this.namedValues = values; - } - - @Override - public String getQueryString(CodecRegistry codecRegistry) { - return query; - } - - @Override - public ByteBuffer[] getValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - if (values == null) return null; - return convert(values, protocolVersion, codecRegistry); - } - - @Override - public Map getNamedValues( - ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - if (namedValues == null) return null; - return convert(namedValues, protocolVersion, codecRegistry); - } - - /** - * The number of values for this statement, that is the size of the array that will be returned by - * {@code getValues}. - * - * @return the number of values. - */ - public int valuesCount() { - if (values != null) return values.length; - else if (namedValues != null) return namedValues.size(); - else return 0; - } - - @Override - public boolean hasValues(CodecRegistry codecRegistry) { - return (values != null && values.length > 0) || (namedValues != null && namedValues.size() > 0); - } - - @Override - public boolean usesNamedValues() { - return namedValues != null && namedValues.size() > 0; - } - - /** - * Returns the {@code i}th positional value as the Java type matching its CQL type. - * - *

Note that, unlike with other driver types like {@link Row}, you can't retrieve named values - * by position. This getter will throw an exception if the statement was created with named values - * (or no values at all). Call {@link #usesNamedValues()} to check the type of values, and {@link - * #getObject(String)} if they are positional. - * - * @param i the index to retrieve. - * @return the {@code i}th value of this statement. - * @throws IllegalStateException if this statement does not have positional values. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - */ - public Object getObject(int i) { - if (values == null) - throw new IllegalStateException("This statement does not have positional values"); - if (i < 0 || i >= values.length) throw new ArrayIndexOutOfBoundsException(i); - return values[i]; - } - - /** - * Returns a named value as the Java type matching its CQL type. - * - * @param name the name of the value to retrieve. - * @return the value that matches the name, or {@code null} if there is no such name. - * @throws IllegalStateException if this statement does not have named values. - */ - public Object getObject(String name) { - if (namedValues == null) - throw new IllegalStateException("This statement does not have named values"); - return namedValues.get(name); - } - - /** - * Returns the names of the named values of this statement. - * - * @return the names of the named values of this statement. - * @throws IllegalStateException if this statement does not have named values. - */ - public Set getValueNames() { - if (namedValues == null) - throw new IllegalStateException("This statement does not have named values"); - return Collections.unmodifiableSet(namedValues.keySet()); - } - - /** - * Returns the routing key for the query. - * - *

Unless the routing key has been explicitly set through {@link #setRoutingKey}, this method - * will return {@code null} to avoid having to parse the query string to retrieve the partition - * key. - * - * @param protocolVersion unused by this implementation (no internal serialization is required to - * compute the key). - * @param codecRegistry unused by this implementation (no internal serialization is required to - * compute the key). - * @return the routing key set through {@link #setRoutingKey} if such a key was set, {@code null} - * otherwise. - * @see Statement#getRoutingKey - */ - @Override - public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - return routingKey; - } - - /** - * Sets the routing key for this query. - * - *

This method allows you to manually provide a routing key for this query. It is thus optional - * since the routing key is only an hint for token aware load balancing policy but is never - * mandatory. - * - *

If the partition key for the query is composite, use the {@link - * #setRoutingKey(ByteBuffer...)} method instead to build the routing key. - * - * @param routingKey the raw (binary) value to use as routing key. - * @return this {@code SimpleStatement} object. - * @see Statement#getRoutingKey - */ - public SimpleStatement setRoutingKey(ByteBuffer routingKey) { - this.routingKey = routingKey; - return this; - } - - /** - * Returns the keyspace this query operates on. - * - *

Unless the keyspace has been explicitly set through {@link #setKeyspace}, this method will - * return {@code null} to avoid having to parse the query string. - * - * @return the keyspace set through {@link #setKeyspace} if such keyspace was set, {@code null} - * otherwise. - * @see Statement#getKeyspace - */ - @Override - public String getKeyspace() { - return keyspace; - } - - /** - * Sets the keyspace this query operates on. - * - *

This method allows you to manually provide a keyspace for this query. It is thus optional - * since the value returned by this method is only an hint for token aware load balancing policy - * but is never mandatory. - * - *

Do note that if the query does not use a fully qualified keyspace, then you do not need to - * set the keyspace through that method as the currently logged in keyspace will be used. - * - * @param keyspace the name of the keyspace this query operates on. - * @return this {@code SimpleStatement} object. - * @see Statement#getKeyspace - */ - public SimpleStatement setKeyspace(String keyspace) { - this.keyspace = keyspace; - return this; - } - - /** - * Sets the routing key for this query. - * - *

See {@link #setRoutingKey(ByteBuffer)} for more information. This method is a variant for - * when the query partition key is composite and thus the routing key must be built from multiple - * values. - * - * @param routingKeyComponents the raw (binary) values to compose to obtain the routing key. - * @return this {@code SimpleStatement} object. - * @see Statement#getRoutingKey - */ - public SimpleStatement setRoutingKey(ByteBuffer... routingKeyComponents) { - this.routingKey = compose(routingKeyComponents); - return this; - } - - /* - * This method performs a best-effort heuristic to guess which codec to use. - * Note that this is not particularly efficient as the codec registry needs to iterate over - * the registered codecs until it finds a suitable one. - */ - private static ByteBuffer[] convert( - Object[] values, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - ByteBuffer[] serializedValues = new ByteBuffer[values.length]; - for (int i = 0; i < values.length; i++) { - Object value = values[i]; - if (value == null) { - // impossible to locate the right codec when object is null, - // so forcing the result to null - serializedValues[i] = null; - } else { - if (value instanceof Token) { - // bypass CodecRegistry for Token instances - serializedValues[i] = ((Token) value).serialize(protocolVersion); - } else { - try { - TypeCodec codec = codecRegistry.codecFor(value); - serializedValues[i] = codec.serialize(value, protocolVersion); - } catch (Exception e) { - // Catch and rethrow to provide a more helpful error message (one that include which - // value is bad) - throw new InvalidTypeException( - String.format( - "Value %d of type %s does not correspond to any CQL3 type", - i, value.getClass()), - e); - } - } - } - } - return serializedValues; - } - - private static Map convert( - Map values, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - Map serializedValues = new HashMap(); - for (Map.Entry entry : values.entrySet()) { - String name = entry.getKey(); - Object value = entry.getValue(); - if (value == null) { - // impossible to locate the right codec when object is null, - // so forcing the result to null - serializedValues.put(name, null); - } else { - if (value instanceof Token) { - // bypass CodecRegistry for Token instances - serializedValues.put(name, ((Token) value).serialize(protocolVersion)); - } else { - try { - TypeCodec codec = codecRegistry.codecFor(value); - serializedValues.put(name, codec.serialize(value, protocolVersion)); - } catch (Exception e) { - // Catch and rethrow to provide a more helpful error message (one that include which - // value is bad) - throw new InvalidTypeException( - String.format( - "Value '%s' of type %s does not correspond to any CQL3 type", - name, value.getClass()), - e); - } - } - } - } - return serializedValues; - } - - /** - * Utility method to assemble different routing key components into a single {@link ByteBuffer}. - * Mainly intended for statements that need to generate a routing key out of their current values. - * - * @param buffers the components of the routing key. - * @return A ByteBuffer containing the serialized routing key - */ - static ByteBuffer compose(ByteBuffer... buffers) { - if (buffers.length == 1) return buffers[0]; - - int totalLength = 0; - for (ByteBuffer bb : buffers) totalLength += 2 + bb.remaining() + 1; - - ByteBuffer out = ByteBuffer.allocate(totalLength); - for (ByteBuffer buffer : buffers) { - ByteBuffer bb = buffer.duplicate(); - putShortLength(out, bb.remaining()); - out.put(bb); - out.put((byte) 0); - } - out.flip(); - return out; - } - - static void putShortLength(ByteBuffer bb, int length) { - bb.put((byte) ((length >> 8) & 0xFF)); - bb.put((byte) (length & 0xFF)); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SnappyCompressor.java b/driver-core/src/main/java/com/datastax/driver/core/SnappyCompressor.java deleted file mode 100644 index 7b9e2b85b8f..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SnappyCompressor.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.DriverInternalError; -import io.netty.buffer.ByteBuf; -import java.io.IOException; -import java.nio.ByteBuffer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.xerial.snappy.Snappy; - -class SnappyCompressor extends FrameCompressor { - - private static final Logger logger = LoggerFactory.getLogger(SnappyCompressor.class); - - static final SnappyCompressor instance; - - static { - SnappyCompressor i; - try { - i = new SnappyCompressor(); - } catch (NoClassDefFoundError e) { - i = null; - logger.warn( - "Cannot find Snappy class, you should make sure the Snappy library is in the classpath if you intend to use it. Snappy compression will not be available for the protocol."); - } catch (Throwable e) { - i = null; - logger.warn( - "Error loading Snappy library ({}). Snappy compression will not be available for the protocol.", - e.toString()); - } - instance = i; - } - - private SnappyCompressor() { - // this would throw java.lang.NoClassDefFoundError if Snappy class - // wasn't found at runtime which should be processed by the calling method - Snappy.getNativeLibraryVersion(); - } - - @Override - Frame compress(Frame frame) throws IOException { - return frame.with(compress(frame.body)); - } - - @Override - ByteBuf compress(ByteBuf buffer) throws IOException { - return buffer.isDirect() ? compressDirect(buffer) : compressHeap(buffer); - } - - private ByteBuf compressDirect(ByteBuf input) throws IOException { - int maxCompressedLength = Snappy.maxCompressedLength(input.readableBytes()); - // If the input is direct we will allocate a direct output buffer as well as this will allow us - // to use - // Snappy.compress(ByteBuffer, ByteBuffer) and so eliminate memory copies. - ByteBuf output = input.alloc().directBuffer(maxCompressedLength); - try { - ByteBuffer in = inputNioBuffer(input); - // Increase reader index. - input.readerIndex(input.writerIndex()); - - ByteBuffer out = outputNioBuffer(output); - int written = Snappy.compress(in, out); - // Set the writer index so the amount of written bytes is reflected - output.writerIndex(output.writerIndex() + written); - } catch (IOException e) { - // release output buffer so we not leak and rethrow exception. - output.release(); - throw e; - } - return output; - } - - private ByteBuf compressHeap(ByteBuf input) throws IOException { - int maxCompressedLength = Snappy.maxCompressedLength(input.readableBytes()); - int inOffset = input.arrayOffset() + input.readerIndex(); - byte[] in = input.array(); - int len = input.readableBytes(); - // Increase reader index. - input.readerIndex(input.writerIndex()); - - // Allocate a heap buffer from the ByteBufAllocator as we may use a PooledByteBufAllocator and - // so - // can eliminate the overhead of allocate a new byte[]. - ByteBuf output = input.alloc().heapBuffer(maxCompressedLength); - try { - // Calculate the correct offset. - int offset = output.arrayOffset() + output.writerIndex(); - byte[] out = output.array(); - int written = Snappy.compress(in, inOffset, len, out, offset); - - // Increase the writerIndex with the written bytes. - output.writerIndex(output.writerIndex() + written); - } catch (IOException e) { - // release output buffer so we not leak and rethrow exception. - output.release(); - throw e; - } - return output; - } - - @Override - Frame decompress(Frame frame) throws IOException { - ByteBuf input = frame.body; - ByteBuf frameBody = input.isDirect() ? decompressDirect(input) : decompressHeap(input); - return frame.with(frameBody); - } - - @Override - ByteBuf decompress(ByteBuf buffer, int uncompressedLength) throws IOException { - // Note that the Snappy algorithm already encodes the uncompressed length, we don't need the - // provided one. - return buffer.isDirect() ? decompressDirect(buffer) : decompressHeap(buffer); - } - - private ByteBuf decompressDirect(ByteBuf input) throws IOException { - ByteBuffer in = inputNioBuffer(input); - // Increase reader index. - input.readerIndex(input.writerIndex()); - - if (!Snappy.isValidCompressedBuffer(in)) - throw new DriverInternalError("Provided frame does not appear to be Snappy compressed"); - - // If the input is direct we will allocate a direct output buffer as well as this will allow us - // to use - // Snappy.compress(ByteBuffer, ByteBuffer) and so eliminate memory copies. - ByteBuf output = input.alloc().directBuffer(Snappy.uncompressedLength(in)); - try { - ByteBuffer out = outputNioBuffer(output); - - int size = Snappy.uncompress(in, out); - // Set the writer index so the amount of written bytes is reflected - output.writerIndex(output.writerIndex() + size); - } catch (IOException e) { - // release output buffer so we not leak and rethrow exception. - output.release(); - throw e; - } - return output; - } - - private ByteBuf decompressHeap(ByteBuf input) throws IOException { - // Not a direct buffer so use byte arrays... - int inOffset = input.arrayOffset() + input.readerIndex(); - byte[] in = input.array(); - int len = input.readableBytes(); - // Increase reader index. - input.readerIndex(input.writerIndex()); - - if (!Snappy.isValidCompressedBuffer(in, inOffset, len)) - throw new DriverInternalError("Provided frame does not appear to be Snappy compressed"); - - // Allocate a heap buffer from the ByteBufAllocator as we may use a PooledByteBufAllocator and - // so - // can eliminate the overhead of allocate a new byte[]. - ByteBuf output = input.alloc().heapBuffer(Snappy.uncompressedLength(in, inOffset, len)); - try { - // Calculate the correct offset. - int offset = output.arrayOffset() + output.writerIndex(); - byte[] out = output.array(); - int written = Snappy.uncompress(in, inOffset, len, out, offset); - - // Increase the writerIndex with the written bytes. - output.writerIndex(output.writerIndex() + written); - } catch (IOException e) { - // release output buffer so we not leak and rethrow exception. - output.release(); - throw e; - } - return output; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SniEndPoint.java b/driver-core/src/main/java/com/datastax/driver/core/SniEndPoint.java deleted file mode 100644 index bbd66513de0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SniEndPoint.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.base.Objects; -import com.google.common.base.Preconditions; -import com.google.common.primitives.UnsignedBytes; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.util.Arrays; -import java.util.Comparator; -import java.util.concurrent.atomic.AtomicLong; - -/** An endpoint to access nodes through a proxy that uses SNI routing. */ -public class SniEndPoint implements EndPoint { - - private static final AtomicLong OFFSET = new AtomicLong(); - - private final InetSocketAddress proxyAddress; - private final String serverName; - - /** - * @param proxyAddress the address of the proxy. If it is {@linkplain - * InetSocketAddress#isUnresolved() unresolved}, each call to {@link #resolve()} will - * re-resolve it, fetch all of its A-records, and if there are more than 1 pick one in a - * round-robin fashion. - * @param serverName the SNI server name. In the context of DSOD, this is the string - * representation of the host id. - */ - public SniEndPoint(InetSocketAddress proxyAddress, String serverName) { - Preconditions.checkNotNull(proxyAddress); - Preconditions.checkNotNull(serverName); - this.proxyAddress = proxyAddress; - this.serverName = serverName; - } - - @Override - public InetSocketAddress resolve() { - if (proxyAddress.isUnresolved()) { - try { - InetAddress[] aRecords = InetAddress.getAllByName(proxyAddress.getHostName()); - if (aRecords.length == 0) { - // Probably never happens, but the JDK docs don't explicitly say so - throw new IllegalArgumentException( - "Could not resolve proxy address " + proxyAddress.getHostName()); - } - // The order of the returned address is unspecified. Sort by IP to make sure we get a true - // round-robin - Arrays.sort(aRecords, IP_COMPARATOR); - int index = (aRecords.length == 1) ? 0 : (int) OFFSET.getAndIncrement() % aRecords.length; - return new InetSocketAddress(aRecords[index], proxyAddress.getPort()); - } catch (UnknownHostException e) { - throw new IllegalArgumentException( - "Could not resolve proxy address " + proxyAddress.getHostName(), e); - } - } else { - return proxyAddress; - } - } - - String getServerName() { - return serverName; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } else if (other instanceof SniEndPoint) { - SniEndPoint that = (SniEndPoint) other; - return this.proxyAddress.equals(that.proxyAddress) && this.serverName.equals(that.serverName); - } else { - return false; - } - } - - @Override - public int hashCode() { - return Objects.hashCode(proxyAddress, serverName); - } - - @Override - public String toString() { - // Note that this uses the original proxy address, so if there are multiple A-records it won't - // show which one was selected. If that turns out to be a problem for debugging, we might need - // to store the result of resolve() in Connection and log that instead of the endpoint. - return proxyAddress.toString() + ":" + serverName; - } - - private static final Comparator IP_COMPARATOR = - new Comparator() { - @Override - public int compare(InetAddress address1, InetAddress address2) { - return UnsignedBytes.lexicographicalComparator() - .compare(address1.getAddress(), address2.getAddress()); - } - }; -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SniEndPointFactory.java b/driver-core/src/main/java/com/datastax/driver/core/SniEndPointFactory.java deleted file mode 100644 index c173a67d697..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SniEndPointFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.net.InetSocketAddress; -import java.util.UUID; - -public class SniEndPointFactory implements EndPointFactory { - - private final InetSocketAddress proxyAddress; - - public SniEndPointFactory(InetSocketAddress proxyAddress) { - this.proxyAddress = proxyAddress; - } - - @Override - public void init(Cluster cluster) {} - - @Override - public EndPoint create(Row peersRow) { - UUID host_id = peersRow.getUUID("host_id"); - return new SniEndPoint(proxyAddress, host_id.toString()); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SniSSLOptions.java b/driver-core/src/main/java/com/datastax/driver/core/SniSSLOptions.java deleted file mode 100644 index a22b876c810..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SniSSLOptions.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.collect.ImmutableList; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.ssl.SslHandler; -import java.net.InetSocketAddress; -import java.util.concurrent.CopyOnWriteArrayList; -import javax.net.ssl.SNIHostName; -import javax.net.ssl.SNIServerName; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLParameters; - -@IgnoreJDK6Requirement -@SuppressWarnings("deprecation") -public class SniSSLOptions extends JdkSSLOptions implements ExtendedRemoteEndpointAwareSslOptions { - - // An offset that gets added to our "fake" ports (see below). We pick this value because it is the - // start of the ephemeral port range. - private static final int FAKE_PORT_OFFSET = 49152; - - private final CopyOnWriteArrayList fakePorts = new CopyOnWriteArrayList(); - - /** - * Creates a new instance. - * - * @param context the SSL context. - * @param cipherSuites the cipher suites to use. - */ - protected SniSSLOptions(SSLContext context, String[] cipherSuites) { - super(context, cipherSuites); - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel) { - throw new AssertionError( - "This class implements RemoteEndpointAwareSSLOptions, this method should not be called"); - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel, EndPoint remoteEndpoint) { - SSLEngine engine = newSSLEngine(channel, remoteEndpoint); - return new SslHandler(engine); - } - - @Override - public SslHandler newSSLHandler(SocketChannel channel, InetSocketAddress remoteEndpoint) { - throw new AssertionError( - "The driver should never call this method on an object that implements " - + this.getClass().getSimpleName()); - } - - protected SSLEngine newSSLEngine( - @SuppressWarnings("unused") SocketChannel channel, EndPoint remoteEndpoint) { - if (!(remoteEndpoint instanceof SniEndPoint)) { - throw new IllegalArgumentException( - String.format( - "Configuration error: can only use %s with SNI end points", - this.getClass().getSimpleName())); - } - SniEndPoint sniEndPoint = (SniEndPoint) remoteEndpoint; - InetSocketAddress address = sniEndPoint.resolve(); - String sniServerName = sniEndPoint.getServerName(); - - // When hostname verification is enabled (with setEndpointIdentificationAlgorithm), the SSL - // engine will try to match the server's certificate against the SNI host name; if that doesn't - // work, it will fall back to the "advisory peer host" passed to createSSLEngine. - // - // In our case, the first check will never succeed because our SNI host name is not the DNS name - // (we use the Cassandra host_id instead). So we *must* set the advisory peer information. - // - // However if we use the address as-is, this leads to another issue: the advisory peer - // information is also used to cache SSL sessions internally. All of our nodes share the same - // proxy address, so the JDK tries to reuse SSL sessions across nodes. But it doesn't update the - // SNI host name every time, so it ends up opening connections to the wrong node. - // - // To avoid that, we create a unique "fake" port for every node. We still get session reuse for - // a given node, but not across nodes. This is safe because the advisory port is only used for - // session caching. - SSLEngine engine = context.createSSLEngine(address.getHostName(), getFakePort(sniServerName)); - engine.setUseClientMode(true); - SSLParameters parameters = engine.getSSLParameters(); - parameters.setServerNames(ImmutableList.of(new SNIHostName(sniServerName))); - parameters.setEndpointIdentificationAlgorithm("HTTPS"); - engine.setSSLParameters(parameters); - if (cipherSuites != null) engine.setEnabledCipherSuites(cipherSuites); - return engine; - } - - private int getFakePort(String sniServerName) { - fakePorts.addIfAbsent(sniServerName); - return FAKE_PORT_OFFSET + fakePorts.indexOf(sniServerName); - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder extends JdkSSLOptions.Builder { - - @Override - public SniSSLOptions.Builder withSSLContext(SSLContext context) { - super.withSSLContext(context); - return this; - } - - @Override - public SniSSLOptions.Builder withCipherSuites(String[] cipherSuites) { - super.withCipherSuites(cipherSuites); - return this; - } - - @Override - public SniSSLOptions build() { - return new SniSSLOptions(context, cipherSuites); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SocketOptions.java b/driver-core/src/main/java/com/datastax/driver/core/SocketOptions.java deleted file mode 100644 index 3517c82cd5e..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SocketOptions.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Options to configure low-level socket options for the connections kept to the Cassandra hosts. - */ -public class SocketOptions { - - /** - * The default connection timeout in milliseconds if none is set explicitly using {@link - * #setConnectTimeoutMillis}. - * - *

That default is of 5 seconds. - */ - public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 5000; - - /** - * The default read timeout in milliseconds if none is set explicitly using {@link - * #setReadTimeoutMillis}. - * - *

That default is of 12 seconds so as to be slightly bigger that the default Cassandra - * timeout. - * - * @see #getReadTimeoutMillis for more details on this timeout. - */ - public static final int DEFAULT_READ_TIMEOUT_MILLIS = 12000; - - private volatile int connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS; - private volatile int readTimeoutMillis = DEFAULT_READ_TIMEOUT_MILLIS; - private volatile Boolean keepAlive; - private volatile Boolean reuseAddress; - private volatile Integer soLinger; - private volatile Boolean tcpNoDelay = Boolean.TRUE; - private volatile Integer receiveBufferSize; - private volatile Integer sendBufferSize; - - /** Creates a new {@code SocketOptions} instance with default values. */ - public SocketOptions() {} - - /** - * The connection timeout in milliseconds. - * - *

As the name implies, the connection timeout defines how long the driver waits to establish a - * new connection to a Cassandra node before giving up. - * - * @return the connection timeout in milliseconds - */ - public int getConnectTimeoutMillis() { - return connectTimeoutMillis; - } - - /** - * Sets the connection timeout in milliseconds. - * - *

The default value is {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS}. - * - * @param connectTimeoutMillis the timeout to set. - * @return this {@code SocketOptions}. - */ - public SocketOptions setConnectTimeoutMillis(int connectTimeoutMillis) { - this.connectTimeoutMillis = connectTimeoutMillis; - return this; - } - - /** - * The per-host read timeout in milliseconds. - * - *

This defines how long the driver will wait for a given Cassandra node to answer a query. - * - *

Please note that this is not the maximum time a call to {@link Session#execute} may block; - * this is the maximum time that a call will wait for one particular Cassandra host, but other - * hosts could be tried if one of them times out, depending on the {@link - * com.datastax.driver.core.policies.RetryPolicy} in use. In other words, a {@link - * Session#execute} call may theoretically wait up to {@code getReadTimeoutMillis() * - * } (though the total number of hosts tried for a given query also - * depends on the {@link com.datastax.driver.core.policies.LoadBalancingPolicy} in use). If you - * want to control how long to wait for a query, use {@link Session#executeAsync} and the {@code - * ResultSetFuture#get(long, TimeUnit)} method. - * - *

Also note that for efficiency reasons, this read timeout is approximate: it has an accuracy - * of up to 100 milliseconds (i.e. it may fire up to 100 milliseconds late). It is not meant to be - * used for precise timeout, but rather as a protection against misbehaving Cassandra nodes. - * - *

- * - * @return the read timeout in milliseconds. - */ - public int getReadTimeoutMillis() { - return readTimeoutMillis; - } - - /** - * Sets the per-host read timeout in milliseconds. - * - *

When setting this value, keep in mind the following: - * - *

    - *
  • it should be higher than the timeout settings used on the Cassandra side ({@code - * *_request_timeout_in_ms} in {@code cassandra.yaml}). - *
  • the read timeout is only approximate and only control the timeout to one Cassandra host, - * not the full query (see {@link #getReadTimeoutMillis} for more details). If a high level - * of precision on the timeout to a request is required, you should use the {@link - * ResultSetFuture#get(long, java.util.concurrent.TimeUnit)} method. - *
- * - *

If you don't call this method, the default value is {@link #DEFAULT_READ_TIMEOUT_MILLIS}. - * - * @param readTimeoutMillis the timeout to set. If it is less than or equal to 0, read timeouts - * are disabled. - * @return this {@code SocketOptions}. - */ - public SocketOptions setReadTimeoutMillis(int readTimeoutMillis) { - this.readTimeoutMillis = readTimeoutMillis; - return this; - } - - /** - * Returns whether TCP keepalive is enabled. - * - * @return the value of the option, or {@code null} if it is not set. - * @see #setKeepAlive(boolean) - */ - public Boolean getKeepAlive() { - return keepAlive; - } - - /** - * Sets whether to enable TCP keepalive. - * - *

By default, this option is not set by the driver. The actual value will be the default from - * the underlying Netty transport (Java NIO or native epoll). - * - * @param keepAlive whether to enable or disable the option. - * @return this {@code SocketOptions}. - * @see java.net.SocketOptions#TCP_NODELAY - */ - public SocketOptions setKeepAlive(boolean keepAlive) { - this.keepAlive = keepAlive; - return this; - } - - /** - * Returns whether reuse-address is enabled. - * - * @return the value of the option, or {@code null} if it is not set. - * @see #setReuseAddress(boolean) - */ - public Boolean getReuseAddress() { - return reuseAddress; - } - - /** - * Sets whether to enable reuse-address. - * - *

By default, this option is not set by the driver. The actual value will be the default from - * the underlying Netty transport (Java NIO or native epoll). - * - * @param reuseAddress whether to enable or disable the option. - * @return this {@code SocketOptions}. - * @see java.net.SocketOptions#SO_REUSEADDR - */ - public SocketOptions setReuseAddress(boolean reuseAddress) { - this.reuseAddress = reuseAddress; - return this; - } - - /** - * Returns the linger-on-close timeout. - * - * @return the value of the option, or {@code null} if it is not set. - * @see #setSoLinger(int) - */ - public Integer getSoLinger() { - return soLinger; - } - - /** - * Sets the linger-on-close timeout. - * - *

By default, this option is not set by the driver. The actual value will be the default from - * the underlying Netty transport (Java NIO or native epoll). - * - * @param soLinger the new value. - * @return this {@code SocketOptions}. - * @see java.net.SocketOptions#SO_LINGER - */ - public SocketOptions setSoLinger(int soLinger) { - this.soLinger = soLinger; - return this; - } - - /** - * Returns whether Nagle's algorithm is disabled. - * - * @return the value of the option ({@code true} means Nagle is disabled), or {@code null} if it - * is not set. - * @see #setTcpNoDelay(boolean) - */ - public Boolean getTcpNoDelay() { - return tcpNoDelay; - } - - /** - * Sets whether to disable Nagle's algorithm. - * - *

By default, this option is set to {@code true} (Nagle disabled). - * - * @param tcpNoDelay whether to enable or disable the option. - * @return this {@code SocketOptions}. - * @see java.net.SocketOptions#TCP_NODELAY - */ - public SocketOptions setTcpNoDelay(boolean tcpNoDelay) { - this.tcpNoDelay = tcpNoDelay; - return this; - } - - /** - * Returns the hint to the size of the underlying buffers for incoming network I/O. - * - * @return the value of the option, or {@code null} if it is not set. - * @see #setReceiveBufferSize(int) - */ - public Integer getReceiveBufferSize() { - return receiveBufferSize; - } - - /** - * Sets a hint to the size of the underlying buffers for incoming network I/O. - * - *

By default, this option is not set by the driver. The actual value will be the default from - * the underlying Netty transport (Java NIO or native epoll). - * - * @param receiveBufferSize the new value. - * @return this {@code SocketOptions}. - * @see java.net.SocketOptions#SO_RCVBUF - */ - public SocketOptions setReceiveBufferSize(int receiveBufferSize) { - this.receiveBufferSize = receiveBufferSize; - return this; - } - - /** - * Returns the hint to the size of the underlying buffers for outgoing network I/O. - * - * @return the value of the option, or {@code null} if it is not set. - * @see #setSendBufferSize(int) - */ - public Integer getSendBufferSize() { - return sendBufferSize; - } - - /** - * Sets a hint to the size of the underlying buffers for outgoing network I/O. - * - *

By default, this option is not set by the driver. The actual value will be the default from - * the underlying Netty transport (Java NIO or native epoll). - * - * @param sendBufferSize the new value. - * @return this {@code SocketOptions}. - * @see java.net.SocketOptions#SO_SNDBUF - */ - public SocketOptions setSendBufferSize(int sendBufferSize) { - this.sendBufferSize = sendBufferSize; - return this; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Statement.java b/driver-core/src/main/java/com/datastax/driver/core/Statement.java deleted file mode 100644 index 48f98cf5c94..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Statement.java +++ /dev/null @@ -1,669 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.NoHostAvailableException; -import com.datastax.driver.core.exceptions.PagingStateException; -import com.datastax.driver.core.exceptions.UnsupportedProtocolVersionException; -import com.datastax.driver.core.policies.LoadBalancingPolicy; -import com.datastax.driver.core.policies.RetryPolicy; -import com.datastax.driver.core.querybuilder.BuiltStatement; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableMap; -import java.nio.ByteBuffer; -import java.util.Collection; -import java.util.Map; - -/** - * An executable query. - * - *

This represents either a {@link RegularStatement}, a {@link BoundStatement} or a {@link - * BatchStatement} along with the querying options (consistency level, whether to trace the query, - * ...). - */ -public abstract class Statement { - - /** - * A special ByteBuffer value that can be used with custom payloads to denote a null value in a - * payload map. - */ - public static final ByteBuffer NULL_PAYLOAD_VALUE = ByteBuffer.allocate(0); - - // An exception to the RegularStatement, BoundStatement or BatchStatement rule above. This is - // used when preparing a statement and for other internal queries. Do not expose publicly. - static final Statement DEFAULT = - new Statement() { - @Override - public ByteBuffer getRoutingKey( - ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - return null; - } - - @Override - public String getKeyspace() { - return null; - } - - @Override - public ConsistencyLevel getConsistencyLevel() { - return ConsistencyLevel.ONE; - } - }; - - private volatile ConsistencyLevel consistency; - private volatile ConsistencyLevel serialConsistency; - private volatile boolean traceQuery; - private volatile int fetchSize; - private volatile long defaultTimestamp = Long.MIN_VALUE; - private volatile int readTimeoutMillis = Integer.MIN_VALUE; - private volatile RetryPolicy retryPolicy; - private volatile ByteBuffer pagingState; - protected volatile Boolean idempotent; - private volatile Map outgoingPayload; - private volatile Host host; - private volatile int nowInSeconds = Integer.MIN_VALUE; - - // We don't want to expose the constructor, because the code relies on this being only sub-classed - // by RegularStatement, BoundStatement and BatchStatement - Statement() {} - - /** - * Sets the consistency level for the query. - * - * @param consistency the consistency level to set. - * @return this {@code Statement} object. - */ - public Statement setConsistencyLevel(ConsistencyLevel consistency) { - this.consistency = consistency; - return this; - } - - /** - * The consistency level for this query. - * - * @return the consistency level for this query, or {@code null} if no consistency level has been - * specified (through {@code setConsistencyLevel}). In the latter case, the default - * consistency level will be used. - */ - public ConsistencyLevel getConsistencyLevel() { - return consistency; - } - - /** - * Sets the serial consistency level for the query. - * - *

The serial consistency level is only used by conditional updates ({@code INSERT}, {@code - * UPDATE} or {@code DELETE} statements with an {@code IF} condition). For those, the serial - * consistency level defines the consistency level of the serial phase (or "paxos" phase) while - * the normal consistency level defines the consistency for the "learn" phase, i.e. what type of - * reads will be guaranteed to see the update right away. For instance, if a conditional write has - * a regular consistency of QUORUM (and is successful), then a QUORUM read is guaranteed to see - * that write. But if the regular consistency of that write is ANY, then only a read with a - * consistency of SERIAL is guaranteed to see it (even a read with consistency ALL is not - * guaranteed to be enough). - * - *

The serial consistency can only be one of {@code ConsistencyLevel.SERIAL} or {@code - * ConsistencyLevel.LOCAL_SERIAL}. While {@code ConsistencyLevel.SERIAL} guarantees full - * linearizability (with other SERIAL updates), {@code ConsistencyLevel.LOCAL_SERIAL} only - * guarantees it in the local data center. - * - *

The serial consistency level is ignored for any query that is not a conditional update - * (serial reads should use the regular consistency level for instance). - * - * @param serialConsistency the serial consistency level to set. - * @return this {@code Statement} object. - * @throws IllegalArgumentException if {@code serialConsistency} is not one of {@code - * ConsistencyLevel.SERIAL} or {@code ConsistencyLevel.LOCAL_SERIAL}. - */ - public Statement setSerialConsistencyLevel(ConsistencyLevel serialConsistency) { - if (!serialConsistency.isSerial()) - throw new IllegalArgumentException( - "Supplied consistency level is not serial: " + serialConsistency); - this.serialConsistency = serialConsistency; - return this; - } - - /** - * The serial consistency level for this query. - * - *

See {@link #setSerialConsistencyLevel(ConsistencyLevel)} for more detail on the serial - * consistency level. - * - * @return the serial consistency level for this query, or {@code null} if no serial consistency - * level has been specified (through {@link #setSerialConsistencyLevel(ConsistencyLevel)}). In - * the latter case, the default serial consistency level will be used. - */ - public ConsistencyLevel getSerialConsistencyLevel() { - return serialConsistency; - } - - /** - * Enables tracing for this query. - * - *

By default (that is unless you call this method), tracing is not enabled. - * - * @return this {@code Statement} object. - */ - public Statement enableTracing() { - this.traceQuery = true; - return this; - } - - /** - * Disables tracing for this query. - * - * @return this {@code Statement} object. - */ - public Statement disableTracing() { - this.traceQuery = false; - return this; - } - - /** - * Returns whether tracing is enabled for this query or not. - * - * @return {@code true} if this query has tracing enabled, {@code false} otherwise. - */ - public boolean isTracing() { - return traceQuery; - } - - /** - * Returns the routing key (in binary raw form) to use for token aware routing of this query. - * - *

The routing key is optional in that implementers are free to return {@code null}. The - * routing key is an hint used for token-aware routing (see {@link - * com.datastax.driver.core.policies.TokenAwarePolicy}), and if provided should correspond to the - * binary value for the query partition key. However, not providing a routing key never causes a - * query to fail and if the load balancing policy used is not token aware, then the routing key - * can be safely ignored. - * - * @param protocolVersion the protocol version that will be used if the actual implementation - * needs to serialize something to compute the key. - * @param codecRegistry the codec registry that will be used if the actual implementation needs to - * serialize something to compute this key. - * @return the routing key for this query or {@code null}. - */ - public abstract ByteBuffer getRoutingKey( - ProtocolVersion protocolVersion, CodecRegistry codecRegistry); - - /** - * Returns the keyspace this query operates on. - * - *

Note that not all query specify on which keyspace they operate on, and so this method can - * always return {@code null}. Firstly, some queries do not operate inside a keyspace: keyspace - * creation, {@code USE} queries, user creation, etc. Secondly, even query that operate within a - * keyspace do not have to specify said keyspace directly, in which case the currently logged in - * keyspace (the one set through a {@code USE} query (or through the use of {@link - * Cluster#connect(String)})). Lastly, as for the routing key, this keyspace information is only a - * hint for token-aware routing (since replica placement depend on the replication strategy in use - * which is a per-keyspace property) and having this method return {@code null} (or even a bogus - * keyspace name) will never cause the query to fail. - * - * @return the keyspace this query operate on if relevant or {@code null}. - */ - public abstract String getKeyspace(); - - /** - * Sets the retry policy to use for this query. - * - *

The default retry policy, if this method is not called, is the one returned by {@link - * com.datastax.driver.core.policies.Policies#getRetryPolicy} in the cluster configuration. This - * method is thus only useful in case you want to punctually override the default policy for this - * request. - * - * @param policy the retry policy to use for this query. - * @return this {@code Statement} object. - */ - public Statement setRetryPolicy(RetryPolicy policy) { - this.retryPolicy = policy; - return this; - } - - /** - * Returns the retry policy sets for this query, if any. - * - * @return the retry policy sets specifically for this query or {@code null} if no query specific - * retry policy has been set through {@link #setRetryPolicy} (in which case the Cluster retry - * policy will apply if necessary). - */ - public RetryPolicy getRetryPolicy() { - return retryPolicy; - } - - /** - * Sets the query fetch size. - * - *

The fetch size controls how much resulting rows will be retrieved simultaneously (the goal - * being to avoid loading too much results in memory for queries yielding large results). Please - * note that while value as low as 1 can be used, it is *highly* discouraged to use such a low - * value in practice as it will yield very poor performance. If in doubt, leaving the default is - * probably a good idea. - * - *

Only {@code SELECT} queries only ever make use of this setting. - * - *

Note that unlike other configuration, when this statement is prepared {@link - * BoundStatement}s created off of {@link PreparedStatement} do not inherit this configuration. - * - *

Note: Paging is not supported with the native protocol version 1. If you call this method - * with {@code fetchSize > 0} and {@code fetchSize != Integer.MAX_VALUE} and the protocol - * version is in use (i.e. if you've force version 1 through {@link - * Cluster.Builder#withProtocolVersion} or you use Cassandra 1.2), you will get {@link - * UnsupportedProtocolVersionException} when submitting this statement for execution. - * - * @param fetchSize the fetch size to use. If {@code fetchSize <e; 0}, the default fetch size - * will be used. To disable paging of the result set, use {@code fetchSize == - * Integer.MAX_VALUE}. - * @return this {@code Statement} object. - */ - public Statement setFetchSize(int fetchSize) { - this.fetchSize = fetchSize; - return this; - } - - /** - * The fetch size for this query. - * - * @return the fetch size for this query. If that value is less or equal to 0 (the default unless - * {@link #setFetchSize} is used), the default fetch size will be used. - */ - public int getFetchSize() { - return fetchSize; - } - - /** - * Sets the default timestamp for this query (in microseconds since the epoch). - * - *

This feature is only available when version {@link ProtocolVersion#V3 V3} or higher of the - * native protocol is in use. With earlier versions, calling this method has no effect. - * - *

The actual timestamp that will be used for this query is, in order of preference: - * - *

    - *
  • the timestamp specified directly in the CQL query string (using the {@code USING - * TIMESTAMP} syntax); - *
  • the timestamp specified through this method, if different from {@link Long#MIN_VALUE}; - *
  • the timestamp returned by the {@link TimestampGenerator} currently in use, if different - * from {@link Long#MIN_VALUE}. - *
- * - * If none of these apply, no timestamp will be sent with the query and Cassandra will generate a - * server-side one (similar to the pre-V3 behavior). - * - *

Note that unlike other configuration, when this statement is prepared {@link - * BoundStatement}s created off of {@link PreparedStatement} do not inherit this configuration. - * - * @param defaultTimestamp the default timestamp for this query (must be strictly positive). - * @return this {@code Statement} object. - * @see Cluster.Builder#withTimestampGenerator(TimestampGenerator) - */ - public Statement setDefaultTimestamp(long defaultTimestamp) { - this.defaultTimestamp = defaultTimestamp; - return this; - } - - /** - * The default timestamp for this query. - * - * @return the default timestamp (in microseconds since the epoch). - */ - public long getDefaultTimestamp() { - return defaultTimestamp; - } - - /** - * Overrides the default per-host read timeout ({@link SocketOptions#getReadTimeoutMillis()}) for - * this statement. - * - *

You should override this only for statements for which the coordinator may allow a longer - * server-side timeout (for example aggregation queries). - * - *

Note that unlike other configuration, when this statement is prepared {@link - * BoundStatement}s created off of {@link PreparedStatement} do not inherit this configuration. - * - * @param readTimeoutMillis the timeout to set. Negative values are not allowed. If it is 0, the - * read timeout will be disabled for this statement. - * @return this {@code Statement} object. - */ - public Statement setReadTimeoutMillis(int readTimeoutMillis) { - Preconditions.checkArgument(readTimeoutMillis >= 0, "read timeout must be >= 0"); - this.readTimeoutMillis = readTimeoutMillis; - return this; - } - - /** - * Return the per-host read timeout that was set for this statement. - * - * @return the timeout. Note that a negative value means that the default {@link - * SocketOptions#getReadTimeoutMillis()} will be used. - */ - public int getReadTimeoutMillis() { - return readTimeoutMillis; - } - - /** - * Sets the paging state. - * - *

This will cause the next execution of this statement to fetch results from a given page, - * rather than restarting from the beginning. - * - *

You get the paging state from a previous execution of the statement (see {@link - * ExecutionInfo#getPagingState()}. This is typically used to iterate in a "stateless" manner - * (e.g. across HTTP requests): - * - *

{@code
-   * Statement st = new SimpleStatement("your query");
-   * ResultSet rs = session.execute(st.setFetchSize(20));
-   * int available = rs.getAvailableWithoutFetching();
-   * for (int i = 0; i < available; i++) {
-   *     Row row = rs.one();
-   *     // Do something with row (e.g. display it to the user...)
-   * }
-   * // Get state and serialize as string or byte[] to store it for the next execution
-   * // (e.g. pass it as a parameter in the "next page" URI)
-   * PagingState pagingState = rs.getExecutionInfo().getPagingState();
-   * String savedState = pagingState.toString();
-   *
-   * // Next execution:
-   * // Get serialized state back (e.g. get URI parameter)
-   * String savedState = ...
-   * Statement st = new SimpleStatement("your query");
-   * st.setPagingState(PagingState.fromString(savedState));
-   * ResultSet rs = session.execute(st.setFetchSize(20));
-   * int available = rs.getAvailableWithoutFetching();
-   * for (int i = 0; i < available; i++) {
-   *     ...
-   * }
-   * }
- * - *

The paging state can only be reused between perfectly identical statements (same query - * string, same bound parameters). Altering the contents of the paging state or trying to set it - * on a different statement will cause this method to fail. - * - *

Note that, due to internal implementation details, the paging state is not portable across - * native protocol versions (see the online documentation - * for more explanations about the native protocol). This means that {@code PagingState} instances - * generated with an old version won't work with a higher version. If that is a problem for you, - * consider using the "unsafe" API (see {@link #setPagingStateUnsafe(byte[])}). - * - * @param pagingState the paging state to set, or {@code null} to remove any state that was - * previously set on this statement. - * @param codecRegistry the codec registry that will be used if this method needs to serialize the - * statement's values in order to check that the paging state matches. - * @return this {@code Statement} object. - * @throws PagingStateException if the paging state does not match this statement. - * @see #setPagingState(PagingState) - */ - public Statement setPagingState(PagingState pagingState, CodecRegistry codecRegistry) { - if (this instanceof BatchStatement) { - throw new UnsupportedOperationException("Cannot set the paging state on a batch statement"); - } else { - if (pagingState == null) { - this.pagingState = null; - } else if (pagingState.matches(this, codecRegistry)) { - this.pagingState = pagingState.getRawState(); - } else { - throw new PagingStateException( - "Paging state mismatch, " - + "this means that either the paging state contents were altered, " - + "or you're trying to apply it to a different statement"); - } - } - return this; - } - - /** - * Sets the paging state. - * - *

This method calls {@link #setPagingState(PagingState, CodecRegistry)} with {@link - * CodecRegistry#DEFAULT_INSTANCE}. Whether you should use this or the other variant depends on - * the type of statement this is called on: - * - *

    - *
  • for a {@link BoundStatement}, the codec registry isn't actually needed, so it's always - * safe to use this method; - *
  • for a {@link SimpleStatement} or {@link BuiltStatement}, you can use this method if you - * use no custom codecs, or if your custom codecs are registered with the default registry. - * Otherwise, use the other method and provide the registry that contains your codecs. - *
- * - * @param pagingState the paging state to set, or {@code null} to remove any state that was - * previously set on this statement. - */ - public Statement setPagingState(PagingState pagingState) { - return setPagingState(pagingState, CodecRegistry.DEFAULT_INSTANCE); - } - - /** - * Sets the paging state. - * - *

Contrary to {@link #setPagingState(PagingState)}, this method takes the "raw" form of the - * paging state (previously extracted with {@link ExecutionInfo#getPagingStateUnsafe()}. It won't - * validate that this statement matches the one that the paging state was extracted from. If the - * paging state was altered in any way, you will get unpredictable behavior from Cassandra - * (ranging from wrong results to a query failure). If you decide to use this variant, it is - * strongly recommended to add your own validation (for example, signing the raw state with a - * private key). - * - * @param pagingState the paging state to set, or {@code null} to remove any state that was - * previously set on this statement. - * @return this {@code Statement} object. - */ - public Statement setPagingStateUnsafe(byte[] pagingState) { - if (pagingState == null) { - this.pagingState = null; - } else { - this.pagingState = ByteBuffer.wrap(pagingState); - } - return this; - } - - ByteBuffer getPagingState() { - return pagingState; - } - - /** - * Sets whether this statement is idempotent. - * - *

See {@link #isIdempotent()} for more explanations about this property. - * - * @param idempotent the new value. - * @return this {@code Statement} object. - */ - public Statement setIdempotent(boolean idempotent) { - this.idempotent = idempotent; - return this; - } - - /** - * Whether this statement is idempotent, i.e. whether it can be applied multiple times without - * changing the result beyond the initial application. - * - *

If a statement is not idempotent, the driver will ensure that it never gets - * executed more than once, which means: - * - *

    - *
  • avoiding {@link RetryPolicy retries} on write timeouts or request errors; - *
  • never scheduling {@link com.datastax.driver.core.policies.SpeculativeExecutionPolicy - * speculative executions}. - *
- * - * (this behavior is implemented in the driver internals, the corresponding policies will not even - * be invoked). - * - *

Note that this method can return {@code null}, in which case the driver will default to - * {@link QueryOptions#getDefaultIdempotence()}. - * - *

By default, this method returns {@code null} for all statements, except for - * - *

    - *
  • {@link BuiltStatement} - value will be inferred from the query: if it updates counters, - * prepends/appends to a list, or uses a function call or {@link - * com.datastax.driver.core.querybuilder.QueryBuilder#raw(String)} anywhere in an inserted - * value, the result will be {@code false}; otherwise it will be {@code true}. - *
  • {@link com.datastax.driver.core.querybuilder.Batch} and {@link BatchStatement}: - *
      - *
    1. If any statement in batch has isIdempotent() false - return false - *
    2. If no statements with isIdempotent() false, but some have isIdempotent() null - - * return null - *
    3. Otherwise - return true - *
    - *
- * - * In all cases, calling {@link #setIdempotent(boolean)} forces a value that overrides calculated - * value. - * - *

Note that when a statement is prepared ({@link Session#prepare(String)}), its idempotence - * flag will be propagated to all {@link PreparedStatement}s created from it. - * - * @return whether this statement is idempotent, or {@code null} to use {@link - * QueryOptions#getDefaultIdempotence()}. - */ - public Boolean isIdempotent() { - return idempotent; - } - - boolean isIdempotentWithDefault(QueryOptions queryOptions) { - Boolean myValue = this.isIdempotent(); - if (myValue != null) return myValue; - else return queryOptions.getDefaultIdempotence(); - } - - /** - * Returns this statement's outgoing payload. Each time this statement is executed, this payload - * will be included in the query request. - * - *

This method returns {@code null} if no payload has been set, otherwise it always returns - * immutable maps. - * - *

This feature is only available with {@link ProtocolVersion#V4} or above. Trying to include - * custom payloads in requests sent by the driver under lower protocol versions will result in an - * {@link com.datastax.driver.core.exceptions.UnsupportedFeatureException} (wrapped in a {@link - * com.datastax.driver.core.exceptions.NoHostAvailableException}). - * - * @return the outgoing payload to include with this statement, or {@code null} if no payload has - * been set. - * @since 2.2 - */ - public Map getOutgoingPayload() { - return outgoingPayload; - } - - /** - * Set the given outgoing payload on this statement. Each time this statement is executed, this - * payload will be included in the query request. - * - *

This method makes a defensive copy of the given map, but its values remain inherently - * mutable. Care should be taken not to modify the original map once it is passed to this method. - * - *

This feature is only available with {@link ProtocolVersion#V4} or above. Trying to include - * custom payloads in requests sent by the driver under lower protocol versions will result in an - * {@link com.datastax.driver.core.exceptions.UnsupportedFeatureException} (wrapped in a {@link - * com.datastax.driver.core.exceptions.NoHostAvailableException}). - * - * @param payload the outgoing payload to include with this statement, or {@code null} to clear - * any previously entered payload. - * @return this {@link Statement} object. - * @since 2.2 - */ - public Statement setOutgoingPayload(Map payload) { - this.outgoingPayload = payload == null ? null : ImmutableMap.copyOf(payload); - return this; - } - - /** - * Returns the number of bytes required to encode this statement. - * - *

The calculated size may be overestimated by a few bytes, but is never underestimated. If the - * size cannot be calculated, this method returns -1. - * - *

Note that the returned value is not cached, but instead recomputed at every method call. - * - * @return the number of bytes required to encode this statement. - */ - public int requestSizeInBytes(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - return -1; - } - - protected static Boolean isBatchIdempotent(Collection statements) { - boolean hasNullIdempotentStatements = false; - for (Statement statement : statements) { - Boolean innerIdempotent = statement.isIdempotent(); - if (innerIdempotent == null) { - hasNullIdempotentStatements = true; - } else if (!innerIdempotent) { - return false; - } - } - return (hasNullIdempotentStatements) ? null : true; - } - - /** - * @return The host configured on this statement, or null if none is configured. - * @see #setHost(Host) - */ - public Host getHost() { - return host; - } - - /** - * Sets the {@link Host} that should handle this query. - * - *

In the general case, use of this method is heavily discouraged and should only be - * used in the following cases: - * - *

    - *
  1. Querying node-local tables, such as tables in the {@code system} and {@code system_views} - * keyspaces. - *
  2. Applying a series of schema changes, where it may be advantageous to execute schema - * changes in sequence on the same node. - *
- * - *

Configuring a specific host causes the configured {@link LoadBalancingPolicy} to be - * completely bypassed. However, if the load balancing policy dictates that the host is at - * distance {@link HostDistance#IGNORED} or there is no active connectivity to the host, the - * request will fail with a {@link NoHostAvailableException}. - * - *

Note that unlike other configuration, when this statement is prepared {@link - * BoundStatement}s created off of {@link PreparedStatement} do not inherit this configuration. - * - * @param host The host that should be used to handle executions of this statement or null to - * delegate to the configured load balancing policy. - * @return this {@code Statement} object. - */ - public Statement setHost(Host host) { - this.host = host; - return this; - } - - /** - * @return a custom "now in seconds" to use when applying the request (for testing purposes). - * {@link Integer#MIN_VALUE} means "no value". - */ - public int getNowInSeconds() { - return nowInSeconds; - } - - /** - * Sets the "now in seconds" to use when applying the request (for testing purposes). {@link - * Integer#MIN_VALUE} means "no value". - */ - public Statement setNowInSeconds(int nowInSeconds) { - this.nowInSeconds = nowInSeconds; - return this; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/StatementWrapper.java b/driver-core/src/main/java/com/datastax/driver/core/StatementWrapper.java deleted file mode 100644 index 06bc5e86336..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/StatementWrapper.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.policies.LoadBalancingPolicy; -import com.datastax.driver.core.policies.RetryPolicy; -import com.datastax.driver.core.policies.SpeculativeExecutionPolicy; -import java.nio.ByteBuffer; -import java.util.Map; - -/** - * Base class for custom {@link Statement} implementations that wrap another statement. - * - *

This is intended for use with a custom {@link RetryPolicy}, {@link LoadBalancingPolicy} or - * {@link SpeculativeExecutionPolicy}. The client code can wrap a statement to "mark" it, or add - * information that will lead to special handling in the policy. - * - *

Example: - * - *

{@code
- * // Define your own subclass
- * public class MyCustomStatement extends StatementWrapper {
- *     public MyCustomStatement(Statement wrapped) {
- *         super(wrapped);
- *     }
- * }
- *
- * // In your load balancing policy, add a special case for that new type
- * public class MyLoadBalancingPolicy implements LoadBalancingPolicy {
- *     public Iterator newQueryPlan(String loggedKeyspace, Statement statement) {
- *         if (statement instanceof MyCustomStatement) {
- *             // return specially crafted plan
- *         } else {
- *             // return default plan
- *         }
- *     }
- * }
- *
- * // The client wraps whenever it wants to trigger the special plan
- * Statement s = new SimpleStatement("...");
- * session.execute(s);                         // will use default plan
- * session.execute(new MyCustomStatement(s));  // will use special plan
- * }
- */ -public abstract class StatementWrapper extends Statement { - private final Statement wrapped; - - /** - * Builds a new instance. - * - * @param wrapped the wrapped statement. - */ - protected StatementWrapper(Statement wrapped) { - this.wrapped = wrapped; - } - - Statement getWrappedStatement() { - // Protect against multiple levels of wrapping (even though there is no practical reason for - // that) - return (wrapped instanceof StatementWrapper) - ? ((StatementWrapper) wrapped).getWrappedStatement() - : wrapped; - } - - @Override - public Statement setConsistencyLevel(ConsistencyLevel consistency) { - wrapped.setConsistencyLevel(consistency); - return this; - } - - @Override - public ConsistencyLevel getConsistencyLevel() { - return wrapped.getConsistencyLevel(); - } - - @Override - public Statement setSerialConsistencyLevel(ConsistencyLevel serialConsistency) { - wrapped.setSerialConsistencyLevel(serialConsistency); - return this; - } - - @Override - public ConsistencyLevel getSerialConsistencyLevel() { - return wrapped.getSerialConsistencyLevel(); - } - - @Override - public Statement enableTracing() { - wrapped.enableTracing(); - return this; - } - - @Override - public Statement disableTracing() { - wrapped.disableTracing(); - return this; - } - - @Override - public boolean isTracing() { - return wrapped.isTracing(); - } - - @Override - public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - return wrapped.getRoutingKey(protocolVersion, codecRegistry); - } - - @Override - public String getKeyspace() { - return wrapped.getKeyspace(); - } - - @Override - public Statement setRetryPolicy(RetryPolicy policy) { - wrapped.setRetryPolicy(policy); - return this; - } - - @Override - public RetryPolicy getRetryPolicy() { - return wrapped.getRetryPolicy(); - } - - @Override - public Statement setFetchSize(int fetchSize) { - wrapped.setFetchSize(fetchSize); - return this; - } - - @Override - public int getFetchSize() { - return wrapped.getFetchSize(); - } - - @Override - public Statement setDefaultTimestamp(long defaultTimestamp) { - wrapped.setDefaultTimestamp(defaultTimestamp); - return this; - } - - @Override - public long getDefaultTimestamp() { - return wrapped.getDefaultTimestamp(); - } - - @Override - public Statement setReadTimeoutMillis(int readTimeoutMillis) { - wrapped.setReadTimeoutMillis(readTimeoutMillis); - return this; - } - - @Override - public int getReadTimeoutMillis() { - return wrapped.getReadTimeoutMillis(); - } - - @Override - public Statement setPagingState(PagingState pagingState, CodecRegistry codecRegistry) { - wrapped.setPagingState(pagingState, codecRegistry); - return this; - } - - @Override - public Statement setPagingState(PagingState pagingState) { - wrapped.setPagingState(pagingState); - return this; - } - - @Override - public Statement setPagingStateUnsafe(byte[] pagingState) { - wrapped.setPagingStateUnsafe(pagingState); - return this; - } - - @Override - public ByteBuffer getPagingState() { - return wrapped.getPagingState(); - } - - @Override - public Statement setIdempotent(boolean idempotent) { - wrapped.setIdempotent(idempotent); - return this; - } - - @Override - public Boolean isIdempotent() { - return wrapped.isIdempotent(); - } - - @Override - public boolean isIdempotentWithDefault(QueryOptions queryOptions) { - return wrapped.isIdempotentWithDefault(queryOptions); - } - - @Override - public Map getOutgoingPayload() { - return wrapped.getOutgoingPayload(); - } - - @Override - public Statement setOutgoingPayload(Map payload) { - wrapped.setOutgoingPayload(payload); - return this; - } - - @Override - public int requestSizeInBytes(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - return wrapped.requestSizeInBytes(protocolVersion, codecRegistry); - } - - @Override - public Host getHost() { - return wrapped.getHost(); - } - - @Override - public Statement setHost(Host host) { - wrapped.setHost(host); - return this; - } - - @Override - public int getNowInSeconds() { - return wrapped.getNowInSeconds(); - } - - @Override - public Statement setNowInSeconds(int nowInSeconds) { - wrapped.setNowInSeconds(nowInSeconds); - return this; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/StreamIdGenerator.java b/driver-core/src/main/java/com/datastax/driver/core/StreamIdGenerator.java deleted file mode 100644 index 182c4ac3940..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/StreamIdGenerator.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLongArray; - -/** - * Manages a set of integer identifiers. - * - *

Clients can borrow an id with {@link #next()}, and return it to the set with {@link - * #release(int)}. It is guaranteed that a given id can't be borrowed by two clients at the same - * time. This class is thread-safe and non-blocking. - * - *

Implementation notes: we use an atomic long array where each bit represents an id. It is set - * to 1 if the id is available, 0 otherwise. When looking for an id, we find a long that has - * remaining 1's and pick the rightmost one. To minimize the average time to find that long, we - * search the array in a round-robin fashion. - */ -class StreamIdGenerator { - static final int MAX_STREAM_PER_CONNECTION_V2 = 128; - static final int MAX_STREAM_PER_CONNECTION_V3 = 32768; - private static final long MAX_UNSIGNED_LONG = -1L; - - static StreamIdGenerator newInstance(ProtocolVersion version) { - return new StreamIdGenerator(streamIdSizeFor(version)); - } - - private static int streamIdSizeFor(ProtocolVersion version) { - switch (version) { - case V1: - case V2: - return 1; - case V3: - case V4: - case V5: - case V6: - return 2; - default: - throw version.unsupported(); - } - } - - private final AtomicLongArray bits; - private final int maxIds; - private final AtomicInteger offset; - - // If a query timeout, we'll stop waiting for it. However in that case, we - // can't release/reuse the ID because we don't know if the response is lost - // or will just come back to use sometimes in the future. In that case, we - // just "mark" the fact that we have one less available ID and marked counts - // how many marks we've put. - private final AtomicInteger marked = new AtomicInteger(0); - - private StreamIdGenerator(int streamIdSizeInBytes) { - // Stream IDs are signed and we only handle positive values - // (negative stream IDs are for server side initiated streams). - maxIds = 1 << (streamIdSizeInBytes * 8 - 1); - - // This is true for 1 byte = 128 streams, and therefore for any higher value - assert maxIds % 64 == 0; - - // We use one bit in our array of longs to represent each stream ID. - bits = new AtomicLongArray(maxIds / 64); - - // Initialize all bits to 1 - for (int i = 0; i < bits.length(); i++) bits.set(i, MAX_UNSIGNED_LONG); - - offset = new AtomicInteger(bits.length() - 1); - } - - public int next() { - int previousOffset, myOffset; - do { - previousOffset = offset.get(); - myOffset = (previousOffset + 1) % bits.length(); - } while (!offset.compareAndSet(previousOffset, myOffset)); - - for (int i = 0; i < bits.length(); i++) { - int j = (i + myOffset) % bits.length(); - - int id = atomicGetAndSetFirstAvailable(j); - if (id >= 0) return id + (64 * j); - } - return -1; - } - - public void release(int streamId) { - atomicClear(streamId / 64, streamId % 64); - } - - public void mark(int streamId) { - marked.incrementAndGet(); - } - - public void unmark(int streamId) { - marked.decrementAndGet(); - } - - public int maxAvailableStreams() { - return maxIds - marked.get(); - } - - // Returns >= 0 if found and set an id, -1 if no bits are available. - private int atomicGetAndSetFirstAvailable(int idx) { - while (true) { - long l = bits.get(idx); - if (l == 0) return -1; - - // Find the position of the right-most 1-bit - int id = Long.numberOfTrailingZeros(l); - if (bits.compareAndSet(idx, l, l ^ mask(id))) return id; - } - } - - private void atomicClear(int idx, int toClear) { - while (true) { - long l = bits.get(idx); - if (bits.compareAndSet(idx, l, l | mask(toClear))) return; - } - } - - private static long mask(int id) { - return 1L << id; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/SystemProperties.java b/driver-core/src/main/java/com/datastax/driver/core/SystemProperties.java deleted file mode 100644 index 7c5541e4295..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/SystemProperties.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Allows overriding internal settings via system properties. - * - *

This is generally reserved for tests or "expert" usage. - */ -class SystemProperties { - private static final Logger logger = LoggerFactory.getLogger(SystemProperties.class); - - static int getInt(String key, int defaultValue) { - String stringValue = System.getProperty(key); - if (stringValue == null) { - logger.debug("{} is undefined, using default value {}", key, defaultValue); - return defaultValue; - } - try { - int value = Integer.parseInt(stringValue); - logger.info("{} is defined, using value {}", key, value); - return value; - } catch (NumberFormatException e) { - logger.warn( - "{} is defined but could not parse value {}, using default value {}", - key, - stringValue, - defaultValue); - return defaultValue; - } - } - - static boolean getBoolean(String key, boolean defaultValue) { - String stringValue = System.getProperty(key); - if (stringValue == null) { - logger.debug("{} is undefined, using default value {}", key, defaultValue); - return defaultValue; - } - try { - boolean value = Boolean.parseBoolean(stringValue); - logger.info("{} is defined, using value {}", key, value); - return value; - } catch (NumberFormatException e) { - logger.warn( - "{} is defined but could not parse value {}, using default value {}", - key, - stringValue, - defaultValue); - return defaultValue; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/TableMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/TableMetadata.java deleted file mode 100644 index f818d4880f9..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/TableMetadata.java +++ /dev/null @@ -1,623 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.collect.ImmutableSortedSet; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.UUID; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** Describes a Table. */ -public class TableMetadata extends AbstractTableMetadata { - - private static final Logger logger = LoggerFactory.getLogger(TableMetadata.class); - - private static final String CF_ID_V2 = "cf_id"; - private static final String CF_ID_V3 = "id"; - - private static final String KEY_VALIDATOR = "key_validator"; - private static final String COMPARATOR = "comparator"; - private static final String VALIDATOR = "default_validator"; - - private static final String KEY_ALIASES = "key_aliases"; - private static final String COLUMN_ALIASES = "column_aliases"; - private static final String VALUE_ALIAS = "value_alias"; - - private static final String DEFAULT_KEY_ALIAS = "key"; - private static final String DEFAULT_COLUMN_ALIAS = "column"; - private static final String DEFAULT_VALUE_ALIAS = "value"; - - private static final String FLAGS = "flags"; - private static final String DENSE = "dense"; - private static final String SUPER = "super"; - private static final String COMPOUND = "compound"; - - private static final String EMPTY_TYPE = "empty"; - - private final Map indexes; - - private final Map views; - - private TableMetadata( - KeyspaceMetadata keyspace, - String name, - UUID id, - List partitionKey, - List clusteringColumns, - Map columns, - Map indexes, - TableOptionsMetadata options, - List clusteringOrder, - VersionNumber cassandraVersion) { - super( - keyspace, - name, - id, - partitionKey, - clusteringColumns, - columns, - options, - clusteringOrder, - cassandraVersion); - this.indexes = indexes; - this.views = new HashMap(); - } - - static TableMetadata build( - KeyspaceMetadata ksm, - Row row, - Map rawCols, - List indexRows, - String nameColumn, - VersionNumber cassandraVersion, - Cluster cluster) { - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - - String name = row.getString(nameColumn); - if (ksm.isVirtual()) { - - // This is always going to be >V3 so key validator can be null - int partitionKeySize = findPartitionKeySize(rawCols.values(), null); - // This is always going to be >V3 so comparator and columnAliases can be null. They are not - // used - int clusteringSize = findClusteringSize(null, rawCols.values(), null, cassandraVersion); - LinkedHashMap columns = new LinkedHashMap(); - List partitionKey = - new ArrayList( - Collections.nCopies(partitionKeySize, null)); - List clusteringColumns = - new ArrayList(Collections.nCopies(clusteringSize, null)); - List clusteringOrder = - new ArrayList( - Collections.nCopies(clusteringSize, null)); - Set otherColumns = new TreeSet(columnMetadataComparator); - TableMetadata tm = - new TableMetadata( - ksm, - name, - new UUID(0L, 0L), - partitionKey, - clusteringColumns, - columns, - Collections.emptyMap(), - null, - clusteringOrder, - cassandraVersion); - - for (ColumnMetadata.Raw rawCol : rawCols.values()) { - DataType dataType; - if (cassandraVersion.getMajor() >= 3) { - dataType = - DataTypeCqlNameParser.parse( - rawCol.dataType, cluster, ksm.getName(), ksm.userTypes, null, false, false); - } else { - dataType = - DataTypeClassNameParser.parseOne(rawCol.dataType, protocolVersion, codecRegistry); - } - ColumnMetadata cm = ColumnMetadata.fromRaw(tm, rawCol, dataType); - - switch (rawCol.kind) { - case PARTITION_KEY: - partitionKey.set(rawCol.position, cm); - break; - case CLUSTERING_COLUMN: - clusteringColumns.set(rawCol.position, cm); - clusteringOrder.set( - rawCol.position, rawCol.isReversed ? ClusteringOrder.DESC : ClusteringOrder.ASC); - break; - default: - otherColumns.add(cm); - break; - } - } - - // Order for virtual table columns should mirror that of normal tables. - for (ColumnMetadata c : partitionKey) columns.put(c.getName(), c); - for (ColumnMetadata c : clusteringColumns) columns.put(c.getName(), c); - for (ColumnMetadata c : otherColumns) columns.put(c.getName(), c); - return tm; - } - - UUID id = null; - - if (cassandraVersion.getMajor() == 2 && cassandraVersion.getMinor() >= 1) - id = row.getUUID(CF_ID_V2); - else if (cassandraVersion.getMajor() > 2) id = row.getUUID(CF_ID_V3); - - DataTypeClassNameParser.ParseResult comparator = null; - DataTypeClassNameParser.ParseResult keyValidator = null; - List columnAliases = null; - - if (cassandraVersion.getMajor() <= 2) { - comparator = - DataTypeClassNameParser.parseWithComposite( - row.getString(COMPARATOR), protocolVersion, codecRegistry); - keyValidator = - DataTypeClassNameParser.parseWithComposite( - row.getString(KEY_VALIDATOR), protocolVersion, codecRegistry); - columnAliases = - cassandraVersion.getMajor() >= 2 || row.getString(COLUMN_ALIASES) == null - ? Collections.emptyList() - : SimpleJSONParser.parseStringList(row.getString(COLUMN_ALIASES)); - } - - int partitionKeySize = findPartitionKeySize(rawCols.values(), keyValidator); - int clusteringSize; - - boolean isDense; - boolean isCompact; - if (cassandraVersion.getMajor() > 2) { - Set flags = row.getSet(FLAGS, String.class); - isDense = flags.contains(DENSE); - boolean isSuper = flags.contains(SUPER); - boolean isCompound = flags.contains(COMPOUND); - isCompact = isSuper || isDense || !isCompound; - boolean isStaticCompact = !isSuper && !isDense && !isCompound; - if (isStaticCompact) { - rawCols = pruneStaticCompactTableColumns(rawCols); - } - if (isDense) { - rawCols = pruneDenseTableColumnsV3(rawCols); - } - clusteringSize = - findClusteringSize(comparator, rawCols.values(), columnAliases, cassandraVersion); - } else { - assert comparator != null; - clusteringSize = - findClusteringSize(comparator, rawCols.values(), columnAliases, cassandraVersion); - isDense = clusteringSize != comparator.types.size() - 1; - if (isDense) { - rawCols = pruneDenseTableColumnsV2(rawCols); - } - isCompact = isDense || !comparator.isComposite; - } - - List partitionKey = - new ArrayList(Collections.nCopies(partitionKeySize, null)); - List clusteringColumns = - new ArrayList(Collections.nCopies(clusteringSize, null)); - List clusteringOrder = - new ArrayList(Collections.nCopies(clusteringSize, null)); - - // We use a linked hashmap because we will keep this in the order of a 'SELECT * FROM ...'. - LinkedHashMap columns = new LinkedHashMap(); - LinkedHashMap indexes = new LinkedHashMap(); - - TableOptionsMetadata options = null; - try { - options = new TableOptionsMetadata(row, isCompact, cassandraVersion); - } catch (RuntimeException e) { - // See ControlConnection#refreshSchema for why we'd rather not probably this further. Since - // table options is one thing - // that tends to change often in Cassandra, it's worth special casing this. - logger.error( - String.format( - "Error parsing schema options for table %s.%s: " - + "Cluster.getMetadata().getKeyspace(\"%s\").getTable(\"%s\").getOptions() will return null", - ksm.getName(), name, ksm.getName(), name), - e); - } - - TableMetadata tm = - new TableMetadata( - ksm, - name, - id, - partitionKey, - clusteringColumns, - columns, - indexes, - options, - clusteringOrder, - cassandraVersion); - - // We use this temporary set just so non PK columns are added in lexicographical order, which is - // the one of a - // 'SELECT * FROM ...' - Set otherColumns = new TreeSet(columnMetadataComparator); - - if (cassandraVersion.getMajor() < 2) { - - assert comparator != null; - assert keyValidator != null; - assert columnAliases != null; - - // In C* 1.2, only the REGULAR columns are in the columns schema table, so we need to add the - // names from - // the aliases (and make sure we handle default aliases). - List keyAliases = - row.getString(KEY_ALIASES) == null - ? Collections.emptyList() - : SimpleJSONParser.parseStringList(row.getString(KEY_ALIASES)); - for (int i = 0; i < partitionKey.size(); i++) { - String alias = - keyAliases.size() > i - ? keyAliases.get(i) - : (i == 0 ? DEFAULT_KEY_ALIAS : DEFAULT_KEY_ALIAS + (i + 1)); - partitionKey.set(i, ColumnMetadata.forAlias(tm, alias, keyValidator.types.get(i))); - } - - for (int i = 0; i < clusteringSize; i++) { - String alias = - columnAliases.size() > i ? columnAliases.get(i) : DEFAULT_COLUMN_ALIAS + (i + 1); - clusteringColumns.set(i, ColumnMetadata.forAlias(tm, alias, comparator.types.get(i))); - clusteringOrder.set( - i, comparator.reversed.get(i) ? ClusteringOrder.DESC : ClusteringOrder.ASC); - } - - // if we're dense, chances are that we have a single regular "value" column with an alias - if (isDense) { - String alias = row.isNull(VALUE_ALIAS) ? DEFAULT_VALUE_ALIAS : row.getString(VALUE_ALIAS); - // ...unless the table does not have any regular column, only primary key columns (JAVA-873) - if (!alias.isEmpty()) { - DataType type = - DataTypeClassNameParser.parseOne( - row.getString(VALIDATOR), protocolVersion, codecRegistry); - otherColumns.add(ColumnMetadata.forAlias(tm, alias, type)); - } - } - } - - for (ColumnMetadata.Raw rawCol : rawCols.values()) { - DataType dataType; - if (cassandraVersion.getMajor() >= 3) { - dataType = - DataTypeCqlNameParser.parse( - rawCol.dataType, cluster, ksm.getName(), ksm.userTypes, null, false, false); - } else { - dataType = - DataTypeClassNameParser.parseOne(rawCol.dataType, protocolVersion, codecRegistry); - } - ColumnMetadata col = ColumnMetadata.fromRaw(tm, rawCol, dataType); - switch (rawCol.kind) { - case PARTITION_KEY: - partitionKey.set(rawCol.position, col); - break; - case CLUSTERING_COLUMN: - clusteringColumns.set(rawCol.position, col); - clusteringOrder.set( - rawCol.position, rawCol.isReversed ? ClusteringOrder.DESC : ClusteringOrder.ASC); - break; - default: - otherColumns.add(col); - break; - } - - // legacy secondary indexes (C* < 3.0) - IndexMetadata index = IndexMetadata.fromLegacy(col, rawCol); - if (index != null) indexes.put(index.getName(), index); - } - - for (ColumnMetadata c : partitionKey) columns.put(c.getName(), c); - for (ColumnMetadata c : clusteringColumns) columns.put(c.getName(), c); - for (ColumnMetadata c : otherColumns) columns.put(c.getName(), c); - - // create secondary indexes (C* >= 3.0) - if (indexRows != null) - for (Row indexRow : indexRows) { - IndexMetadata index = IndexMetadata.fromRow(tm, indexRow); - indexes.put(index.getName(), index); - } - - return tm; - } - - /** - * Upon migration from thrift to CQL, we internally create a pair of surrogate clustering/regular - * columns for compact static tables. These columns shouldn't be exposed to the user but are - * currently returned by C*. We also need to remove the static keyword for all other columns in - * the table. - */ - private static Map pruneStaticCompactTableColumns( - Map rawCols) { - Collection cols = rawCols.values(); - Iterator it = cols.iterator(); - while (it.hasNext()) { - ColumnMetadata.Raw col = it.next(); - if (col.kind == ColumnMetadata.Raw.Kind.CLUSTERING_COLUMN) { - // remove "column1 text" clustering column - it.remove(); - } else if (col.kind == ColumnMetadata.Raw.Kind.REGULAR) { - // remove "value blob" regular column - it.remove(); - } else if (col.kind == ColumnMetadata.Raw.Kind.STATIC) { - // remove "static" keyword - col.kind = ColumnMetadata.Raw.Kind.REGULAR; - } - } - return rawCols; - } - - /** - * Upon migration from thrift to CQL, we internally create a surrogate column "value" of type - * EmptyType for dense tables. This column shouldn't be exposed to the user but is currently - * returned by C*. - */ - private static Map pruneDenseTableColumnsV3( - Map rawCols) { - Collection cols = rawCols.values(); - Iterator it = cols.iterator(); - while (it.hasNext()) { - ColumnMetadata.Raw col = it.next(); - if (col.kind == ColumnMetadata.Raw.Kind.REGULAR && col.dataType.equals(EMPTY_TYPE)) { - // remove "value empty" regular column - it.remove(); - } - } - return rawCols; - } - - private static Map pruneDenseTableColumnsV2( - Map rawCols) { - Collection cols = rawCols.values(); - Iterator it = cols.iterator(); - while (it.hasNext()) { - ColumnMetadata.Raw col = it.next(); - if (col.kind == ColumnMetadata.Raw.Kind.COMPACT_VALUE && col.name.isEmpty()) { - // remove "" blob regular COMPACT_VALUE column - it.remove(); - } - } - return rawCols; - } - - private static int findPartitionKeySize( - Collection cols, DataTypeClassNameParser.ParseResult keyValidator) { - // C* 1.2, 2.0, 2.1 and 2.2 - if (keyValidator != null) return keyValidator.types.size(); - // C* 3.0 onwards - int maxId = -1; - for (ColumnMetadata.Raw col : cols) - if (col.kind == ColumnMetadata.Raw.Kind.PARTITION_KEY) maxId = Math.max(maxId, col.position); - return maxId + 1; - } - - private static int findClusteringSize( - DataTypeClassNameParser.ParseResult comparator, - Collection cols, - List columnAliases, - VersionNumber cassandraVersion) { - // In 2.0 onwards, this is relatively easy, we just find the biggest 'position' amongst the - // clustering columns. - // For 1.2 however, this is slightly more subtle: we need to infer it based on whether the - // comparator is composite or not, and whether we have - // regular columns or not. - if (cassandraVersion.getMajor() >= 2) { - int maxId = -1; - for (ColumnMetadata.Raw col : cols) - if (col.kind == ColumnMetadata.Raw.Kind.CLUSTERING_COLUMN) - maxId = Math.max(maxId, col.position); - return maxId + 1; - } else { - int size = comparator.types.size(); - if (comparator.isComposite) - return !comparator.collections.isEmpty() - || (columnAliases.size() == size - 1 - && comparator.types.get(size - 1).equals(DataType.text())) - ? size - 1 - : size; - else - // We know cols only has the REGULAR ones for 1.2 - return !columnAliases.isEmpty() || cols.isEmpty() ? size : 0; - } - } - - /** - * Returns metadata on a index of this table. - * - * @param name the name of the index to retrieve ({@code name} will be interpreted as a - * case-insensitive identifier unless enclosed in double-quotes, see {@link Metadata#quote}). - * @return the metadata for the {@code name} index if it exists, or {@code null} otherwise. - */ - public IndexMetadata getIndex(String name) { - return indexes.get(Metadata.handleId(name)); - } - - /** - * Returns all indexes based on this table. - * - * @return all indexes based on this table. - */ - public Collection getIndexes() { - return Collections.unmodifiableCollection(indexes.values()); - } - - /** - * Returns metadata on a view of this table. - * - * @param name the name of the view to retrieve ({@code name} will be interpreted as a - * case-insensitive identifier unless enclosed in double-quotes, see {@link Metadata#quote}). - * @return the metadata for the {@code name} view if it exists, or {@code null} otherwise. - */ - public MaterializedViewMetadata getView(String name) { - return views.get(Metadata.handleId(name)); - } - - /** - * Returns all views based on this table. - * - * @return all views based on this table. - */ - public Collection getViews() { - return Collections.unmodifiableCollection(views.values()); - } - - void add(MaterializedViewMetadata view) { - views.put(view.getName(), view); - } - - /** - * Returns a {@code String} containing CQL queries representing this table and the index on it. - * - *

In other words, this method returns the queries that would allow you to recreate the schema - * of this table, along with the indexes and views defined on this table, if any. - * - *

Note that the returned String is formatted to be human readable (for some definition of - * human readable at least). - * - * @return the CQL queries representing this table schema as a {code String}. - */ - @Override - public String exportAsString() { - StringBuilder sb = new StringBuilder(); - - sb.append(super.exportAsString()); - - if (!indexes.isEmpty()) { - sb.append('\n'); - - Iterator indexIt = indexes.values().iterator(); - while (indexIt.hasNext()) { - IndexMetadata index = indexIt.next(); - sb.append('\n').append(index.asCQLQuery()); - if (indexIt.hasNext()) { - sb.append('\n'); - } - } - } - - if (!views.isEmpty()) { - sb.append('\n'); - - Iterator viewsIt = - ImmutableSortedSet.orderedBy(AbstractTableMetadata.byNameComparator) - .addAll(views.values()) - .build() - .iterator(); - while (viewsIt.hasNext()) { - AbstractTableMetadata view = viewsIt.next(); - sb.append('\n').append(view.exportAsString()); - if (viewsIt.hasNext()) { - sb.append('\n'); - } - } - } - - return sb.toString(); - } - - @Override - protected String asCQLQuery(boolean formatted) { - StringBuilder sb = new StringBuilder(); - if (isVirtual()) { - sb.append("/* VIRTUAL "); - } else { - sb.append("CREATE "); - } - sb.append("TABLE ") - .append(Metadata.quoteIfNecessary(keyspace.getName())) - .append('.') - .append(Metadata.quoteIfNecessary(name)) - .append(" ("); - if (formatted) { - spaceOrNewLine(sb, true); - } - for (ColumnMetadata cm : columns.values()) { - sb.append(cm).append(','); - spaceOrNewLine(sb, formatted); - } - // PK - sb.append("PRIMARY KEY ("); - if (partitionKey.size() == 1) { - sb.append(Metadata.quoteIfNecessary(partitionKey.get(0).getName())); - } else { - sb.append('('); - boolean first = true; - for (ColumnMetadata cm : partitionKey) { - if (first) first = false; - else sb.append(", "); - sb.append(Metadata.quoteIfNecessary(cm.getName())); - } - sb.append(')'); - } - for (ColumnMetadata cm : clusteringColumns) - sb.append(", ").append(Metadata.quoteIfNecessary(cm.getName())); - sb.append(')'); - newLine(sb, formatted); - // end PK - sb.append(") "); - appendOptions(sb, formatted); - if (isVirtual()) { - sb.append(" */"); - } - return sb.toString(); - } - - @Override - public boolean equals(Object other) { - if (other == this) return true; - if (!(other instanceof TableMetadata)) return false; - - TableMetadata that = (TableMetadata) other; - - return MoreObjects.equal(this.name, that.name) - && MoreObjects.equal(this.id, that.id) - && MoreObjects.equal(this.partitionKey, that.partitionKey) - && MoreObjects.equal(this.clusteringColumns, that.clusteringColumns) - && MoreObjects.equal(this.columns, that.columns) - && MoreObjects.equal(this.options, that.options) - && MoreObjects.equal(this.clusteringOrder, that.clusteringOrder) - && MoreObjects.equal(this.indexes, that.indexes) - && MoreObjects.equal(this.views, that.views); - } - - @Override - public int hashCode() { - return MoreObjects.hashCode( - name, - id, - partitionKey, - clusteringColumns, - columns, - options, - clusteringOrder, - indexes, - views); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/TableOptionsMetadata.java b/driver-core/src/main/java/com/datastax/driver/core/TableOptionsMetadata.java deleted file mode 100644 index cf8424e7ac0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/TableOptionsMetadata.java +++ /dev/null @@ -1,491 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.collect.ImmutableMap; -import java.nio.ByteBuffer; -import java.util.Map; - -public class TableOptionsMetadata { - - private static final String COMMENT = "comment"; - private static final String READ_REPAIR_CHANCE = "read_repair_chance"; - private static final String DCLOCAL_READ_REPAIR_CHANCE = "dclocal_read_repair_chance"; - private static final String READ_REPAIR = "read_repair"; - private static final String LOCAL_READ_REPAIR_CHANCE = "local_read_repair_chance"; - private static final String REPLICATE_ON_WRITE = "replicate_on_write"; - private static final String GC_GRACE = "gc_grace_seconds"; - private static final String BF_FP_CHANCE = "bloom_filter_fp_chance"; - private static final String CACHING = "caching"; - private static final String COMPACTION = "compaction"; - private static final String COMPACTION_CLASS = "compaction_strategy_class"; - private static final String COMPACTION_OPTIONS = "compaction_strategy_options"; - private static final String POPULATE_CACHE_ON_FLUSH = "populate_io_cache_on_flush"; - private static final String COMPRESSION = "compression"; - private static final String COMPRESSION_PARAMS = "compression_parameters"; - private static final String MEMTABLE_FLUSH_PERIOD_MS = "memtable_flush_period_in_ms"; - private static final String DEFAULT_TTL = "default_time_to_live"; - private static final String SPECULATIVE_RETRY = "speculative_retry"; - private static final String INDEX_INTERVAL = "index_interval"; - private static final String MIN_INDEX_INTERVAL = "min_index_interval"; - private static final String MAX_INDEX_INTERVAL = "max_index_interval"; - private static final String CRC_CHECK_CHANCE = "crc_check_chance"; - private static final String EXTENSIONS = "extensions"; - private static final String CDC = "cdc"; - private static final String ADDITIONAL_WRITE_POLICY = "additional_write_policy"; - - private static final boolean DEFAULT_REPLICATE_ON_WRITE = true; - private static final double DEFAULT_BF_FP_CHANCE = 0.01; - private static final boolean DEFAULT_POPULATE_CACHE_ON_FLUSH = false; - private static final int DEFAULT_MEMTABLE_FLUSH_PERIOD = 0; - private static final int DEFAULT_DEFAULT_TTL = 0; - private static final String DEFAULT_SPECULATIVE_RETRY = "NONE"; - private static final int DEFAULT_INDEX_INTERVAL = 128; - private static final int DEFAULT_MIN_INDEX_INTERVAL = 128; - private static final int DEFAULT_MAX_INDEX_INTERVAL = 2048; - private static final double DEFAULT_CRC_CHECK_CHANCE = 1.0; - private static final boolean DEFAULT_CDC = false; - private static final String DEFAULT_READ_REPAIR = "BLOCKING"; - private static final String DEFAULT_ADDITIONAL_WRITE_POLICY = "99p"; - - private final boolean isCompactStorage; - - private final String comment; - private final double readRepairChance; - private final double localReadRepairChance; - private final String readRepair; - private final boolean replicateOnWrite; - private final int gcGrace; - private final double bfFpChance; - private final Map caching; - private final boolean populateCacheOnFlush; - private final int memtableFlushPeriodMs; - private final int defaultTTL; - private final String speculativeRetry; - private final Integer indexInterval; - private final Integer minIndexInterval; - private final Integer maxIndexInterval; - private final Map compaction; - private final Map compression; - private final Double crcCheckChance; - private final Map extensions; - private final boolean cdc; - private final String additionalWritePolicy; - - TableOptionsMetadata(Row row, boolean isCompactStorage, VersionNumber version) { - - boolean is120 = version.getMajor() < 2; - boolean is200 = version.getMajor() == 2 && version.getMinor() == 0; - boolean is210 = version.getMajor() == 2 && version.getMinor() >= 1; - boolean is400OrHigher = version.getMajor() > 3; - boolean is380OrHigher = is400OrHigher || version.getMajor() == 3 && version.getMinor() >= 8; - boolean is300OrHigher = version.getMajor() > 2; - boolean is210OrHigher = is210 || is300OrHigher; - - this.isCompactStorage = isCompactStorage; - this.comment = isNullOrAbsent(row, COMMENT) ? "" : row.getString(COMMENT); - this.readRepairChance = row.getDouble(READ_REPAIR_CHANCE); - - if (is400OrHigher) this.readRepair = row.getString(READ_REPAIR); - else this.readRepair = DEFAULT_READ_REPAIR; - - if (is300OrHigher) this.localReadRepairChance = row.getDouble(DCLOCAL_READ_REPAIR_CHANCE); - else this.localReadRepairChance = row.getDouble(LOCAL_READ_REPAIR_CHANCE); - - this.replicateOnWrite = - is210OrHigher || isNullOrAbsent(row, REPLICATE_ON_WRITE) - ? DEFAULT_REPLICATE_ON_WRITE - : row.getBool(REPLICATE_ON_WRITE); - this.gcGrace = row.getInt(GC_GRACE); - this.bfFpChance = - isNullOrAbsent(row, BF_FP_CHANCE) ? DEFAULT_BF_FP_CHANCE : row.getDouble(BF_FP_CHANCE); - - this.populateCacheOnFlush = - isNullOrAbsent(row, POPULATE_CACHE_ON_FLUSH) - ? DEFAULT_POPULATE_CACHE_ON_FLUSH - : row.getBool(POPULATE_CACHE_ON_FLUSH); - this.memtableFlushPeriodMs = - is120 || isNullOrAbsent(row, MEMTABLE_FLUSH_PERIOD_MS) - ? DEFAULT_MEMTABLE_FLUSH_PERIOD - : row.getInt(MEMTABLE_FLUSH_PERIOD_MS); - this.defaultTTL = - is120 || isNullOrAbsent(row, DEFAULT_TTL) ? DEFAULT_DEFAULT_TTL : row.getInt(DEFAULT_TTL); - this.speculativeRetry = - is120 || isNullOrAbsent(row, SPECULATIVE_RETRY) - ? DEFAULT_SPECULATIVE_RETRY - : row.getString(SPECULATIVE_RETRY); - - if (is200) - this.indexInterval = - isNullOrAbsent(row, INDEX_INTERVAL) ? DEFAULT_INDEX_INTERVAL : row.getInt(INDEX_INTERVAL); - else this.indexInterval = null; - - if (is210OrHigher) { - this.minIndexInterval = - isNullOrAbsent(row, MIN_INDEX_INTERVAL) - ? DEFAULT_MIN_INDEX_INTERVAL - : row.getInt(MIN_INDEX_INTERVAL); - this.maxIndexInterval = - isNullOrAbsent(row, MAX_INDEX_INTERVAL) - ? DEFAULT_MAX_INDEX_INTERVAL - : row.getInt(MAX_INDEX_INTERVAL); - } else { - this.minIndexInterval = null; - this.maxIndexInterval = null; - } - - if (is300OrHigher) { - this.caching = ImmutableMap.copyOf(row.getMap(CACHING, String.class, String.class)); - } else if (is210) { - this.caching = ImmutableMap.copyOf(SimpleJSONParser.parseStringMap(row.getString(CACHING))); - } else { - this.caching = ImmutableMap.of("keys", row.getString(CACHING)); - } - - if (is300OrHigher) - this.compaction = ImmutableMap.copyOf(row.getMap(COMPACTION, String.class, String.class)); - else { - this.compaction = - ImmutableMap.builder() - .put("class", row.getString(COMPACTION_CLASS)) - .putAll(SimpleJSONParser.parseStringMap(row.getString(COMPACTION_OPTIONS))) - .build(); - } - - if (is300OrHigher) - this.compression = ImmutableMap.copyOf(row.getMap(COMPRESSION, String.class, String.class)); - else - this.compression = - ImmutableMap.copyOf(SimpleJSONParser.parseStringMap(row.getString(COMPRESSION_PARAMS))); - - if (is300OrHigher) - this.crcCheckChance = - isNullOrAbsent(row, CRC_CHECK_CHANCE) - ? DEFAULT_CRC_CHECK_CHANCE - : row.getDouble(CRC_CHECK_CHANCE); - else this.crcCheckChance = null; - - if (is300OrHigher) - this.extensions = ImmutableMap.copyOf(row.getMap(EXTENSIONS, String.class, ByteBuffer.class)); - else this.extensions = ImmutableMap.of(); - - if (is380OrHigher) this.cdc = isNullOrAbsent(row, CDC) ? DEFAULT_CDC : row.getBool(CDC); - else this.cdc = DEFAULT_CDC; - - if (is400OrHigher) this.additionalWritePolicy = row.getString(ADDITIONAL_WRITE_POLICY); - else this.additionalWritePolicy = DEFAULT_ADDITIONAL_WRITE_POLICY; - } - - private static boolean isNullOrAbsent(Row row, String name) { - return row.getColumnDefinitions().getIndexOf(name) < 0 || row.isNull(name); - } - - /** - * Returns whether the table uses the {@code COMPACT STORAGE} option. - * - * @return whether the table uses the {@code COMPACT STORAGE} option. - */ - public boolean isCompactStorage() { - return isCompactStorage; - } - - /** - * Returns the commentary set for this table. - * - * @return the commentary set for this table, or {@code null} if noe has been set. - */ - public String getComment() { - return comment; - } - - /** - * Returns the chance with which a read repair is triggered for this table. - * - * @return the read repair chance set for table (in [0.0, 1.0]). - */ - public double getReadRepairChance() { - return readRepairChance; - } - - /** - * Returns the read_repair option for this table. NOTE: this is a Cassandra® 4.0 and newer - * option (described here: - * http://cassandra.apache.org/doc/latest/operating/read_repair.html). Possible values are - * {@code BLOCKING} or {@code NONE}, with the default being {@code BLOCKING}. - * - * @return the read repair option (either {@code BLOCKING} or {@code NONE}). - */ - public String getReadRepair() { - return readRepair; - } - - /** - * Returns the cluster local read repair chance set for this table. - * - * @return the local read repair chance set for table (in [0.0, 1.0]). - */ - public double getLocalReadRepairChance() { - return localReadRepairChance; - } - - /** - * Returns whether replicateOnWrite is set for this table. - * - *

This is only meaningful for tables holding counters. - * - * @return whether replicateOnWrite is set for this table. - */ - public boolean getReplicateOnWrite() { - return replicateOnWrite; - } - - /** - * Returns the tombstone garbage collection grace time in seconds for this table. - * - * @return the tombstone garbage collection grace time in seconds for this table. - */ - public int getGcGraceInSeconds() { - return gcGrace; - } - - /** - * Returns the false positive chance for the Bloom filter of this table. - * - * @return the Bloom filter false positive chance for this table (in [0.0, 1.0]). - */ - public double getBloomFilterFalsePositiveChance() { - return bfFpChance; - } - - /** - * Returns the caching options for this table. - * - * @return an immutable map containing the caching options for this table. - */ - public Map getCaching() { - return caching; - } - - /** - * Whether the populate I/O cache on flush is set on this table. - * - * @return whether the populate I/O cache on flush is set on this table. - */ - public boolean getPopulateIOCacheOnFlush() { - return populateCacheOnFlush; - } - - /* - * Returns the memtable flush period (in milliseconds) option for this table. - *

- * Note: this option is not available in Cassandra 1.2 and will return 0 (no periodic - * flush) when connected to 1.2 nodes. - * - * @return the memtable flush period option for this table or 0 if no - * periodic flush is configured. - */ - public int getMemtableFlushPeriodInMs() { - return memtableFlushPeriodMs; - } - - /** - * Returns the default TTL for this table. - * - *

Note: this option is not available in Cassandra 1.2 and will return 0 (no default TTL) when - * connected to 1.2 nodes. - * - * @return the default TTL for this table or 0 if no default TTL is configured. - */ - public int getDefaultTimeToLive() { - return defaultTTL; - } - - /** - * Returns the speculative retry option for this table. - * - *

Note: this option is not available in Cassandra 1.2 and will return "NONE" (no speculative - * retry) when connected to 1.2 nodes. - * - * @return the speculative retry option this table. - */ - public String getSpeculativeRetry() { - return speculativeRetry; - } - - /** - * Returns the index interval option for this table. - * - *

Note: this option is not available in Cassandra 1.2 (more precisely, it is not configurable - * per-table) and will return 128 (the default index interval) when connected to 1.2 nodes. It is - * deprecated in Cassandra 2.1 and above, and will therefore return {@code null} for 2.1 nodes. - * - * @return the index interval option for this table. - */ - public Integer getIndexInterval() { - return indexInterval; - } - - /** - * Returns the minimum index interval option for this table. - * - *

Note: this option is available in Cassandra 2.1 and above, and will return {@code null} for - * earlier versions. - * - * @return the minimum index interval option for this table. - */ - public Integer getMinIndexInterval() { - return minIndexInterval; - } - - /** - * Returns the maximum index interval option for this table. - * - *

Note: this option is available in Cassandra 2.1 and above, and will return {@code null} for - * earlier versions. - * - * @return the maximum index interval option for this table. - */ - public Integer getMaxIndexInterval() { - return maxIndexInterval; - } - - /** - * When compression is enabled, this option defines the probability with which checksums for - * compressed blocks are checked during reads. The default value for this options is 1.0 (always - * check). - * - *

Note that this option is available in Cassandra 3.0.0 and above, when it became a - * "top-level" table option, whereas previously it was a suboption of the {@link #getCompression() - * compression} option. - * - *

For Cassandra versions prior to 3.0.0, this method always returns {@code null}. - * - * @return the probability with which checksums for compressed blocks are checked during reads - */ - public Double getCrcCheckChance() { - return crcCheckChance; - } - - /** - * Returns the compaction options for this table. - * - * @return an immutable map containing the compaction options for this table. - */ - public Map getCompaction() { - return compaction; - } - - /** - * Returns the compression options for this table. - * - * @return an immutable map containing the compression options for this table. - */ - public Map getCompression() { - return compression; - } - - /** - * Returns the extension options for this table. - * - *

For Cassandra versions prior to 3.0.0, this method always returns an empty map. - * - * @return an immutable map containing the extension options for this table. - */ - public Map getExtensions() { - return extensions; - } - - /** - * Returns whether or not change data capture is enabled for this table. - * - *

For Cassandra versions prior to 3.8.0, this method always returns false. - * - * @return whether or not change data capture is enabled for this table. - */ - public boolean isCDC() { - return cdc; - } - - /** - * The threshold at which a cheap quorum write will be upgraded to include transient replicas. - * - *

This option is only available in Cassandra® 4.0 and above. Default value is {@code 99p}. - * - * @return The additional write policy for this table (ex. '99p'). - */ - public String getAdditionalWritePolicy() { - return additionalWritePolicy; - } - - @Override - public boolean equals(Object other) { - if (other == this) return true; - if (!(other instanceof TableOptionsMetadata)) return false; - - TableOptionsMetadata that = (TableOptionsMetadata) other; - return this.isCompactStorage == that.isCompactStorage - && MoreObjects.equal(this.comment, that.comment) - && this.readRepairChance == that.readRepairChance - && this.localReadRepairChance == that.localReadRepairChance - && MoreObjects.equal(this.readRepair, that.readRepair) - && this.replicateOnWrite == that.replicateOnWrite - && this.gcGrace == that.gcGrace - && this.bfFpChance == that.bfFpChance - && MoreObjects.equal(this.caching, that.caching) - && this.populateCacheOnFlush == that.populateCacheOnFlush - && this.memtableFlushPeriodMs == that.memtableFlushPeriodMs - && this.defaultTTL == that.defaultTTL - && this.cdc == that.cdc - && MoreObjects.equal(this.speculativeRetry, that.speculativeRetry) - && MoreObjects.equal(this.indexInterval, that.indexInterval) - && MoreObjects.equal(this.minIndexInterval, that.minIndexInterval) - && MoreObjects.equal(this.maxIndexInterval, that.maxIndexInterval) - && MoreObjects.equal(this.compaction, that.compaction) - && MoreObjects.equal(this.compression, that.compression) - && MoreObjects.equal(this.crcCheckChance, that.crcCheckChance) - && MoreObjects.equal(this.additionalWritePolicy, that.additionalWritePolicy) - && MoreObjects.equal(this.extensions, that.extensions); - } - - @Override - public int hashCode() { - return MoreObjects.hashCode( - isCompactStorage, - comment, - readRepairChance, - localReadRepairChance, - readRepair, - replicateOnWrite, - gcGrace, - bfFpChance, - caching, - populateCacheOnFlush, - memtableFlushPeriodMs, - defaultTTL, - speculativeRetry, - indexInterval, - minIndexInterval, - maxIndexInterval, - compaction, - compression, - crcCheckChance, - extensions, - cdc, - additionalWritePolicy); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ThreadLocalMonotonicTimestampGenerator.java b/driver-core/src/main/java/com/datastax/driver/core/ThreadLocalMonotonicTimestampGenerator.java deleted file mode 100644 index 612f2208e1f..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ThreadLocalMonotonicTimestampGenerator.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import java.util.concurrent.TimeUnit; - -/** - * A timestamp generator that guarantees monotonically increasing timestamps on a per-thread basis, - * and logs warnings when timestamps drift in the future. - * - *

Beware that there is a risk of timestamp collision with this generator when accessed by more - * than one thread at a time; only use it when threads are not in direct competition for timestamp - * ties (i.e., they are executing independent statements). - * - * @see AbstractMonotonicTimestampGenerator - */ -public class ThreadLocalMonotonicTimestampGenerator extends LoggingMonotonicTimestampGenerator { - - // We're deliberately avoiding an anonymous subclass with initialValue(), because this can - // introduce - // classloader leaks in managed environments like Tomcat - private final ThreadLocal lastRef = new ThreadLocal(); - - /** - * Creates a new instance with a warning threshold and warning interval of one second. - * - * @see #ThreadLocalMonotonicTimestampGenerator(long, TimeUnit, long, TimeUnit) - */ - public ThreadLocalMonotonicTimestampGenerator() { - this(1, TimeUnit.SECONDS, 1, TimeUnit.SECONDS); - } - - /** - * Creates a new instance. - * - * @param warningThreshold how far in the future timestamps are allowed to drift before a warning - * is logged. - * @param warningThresholdUnit the unit for {@code warningThreshold}. - * @param warningInterval how often the warning will be logged if timestamps keep drifting above - * the threshold. - * @param warningIntervalUnit the unit for {@code warningIntervalUnit}. - */ - public ThreadLocalMonotonicTimestampGenerator( - long warningThreshold, - TimeUnit warningThresholdUnit, - long warningInterval, - TimeUnit warningIntervalUnit) { - super(warningThreshold, warningThresholdUnit, warningInterval, warningIntervalUnit); - } - - @Override - public long next() { - Long last = this.lastRef.get(); - if (last == null) last = 0L; - - long next = computeNext(last); - - this.lastRef.set(next); - return next; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/ThreadingOptions.java b/driver-core/src/main/java/com/datastax/driver/core/ThreadingOptions.java deleted file mode 100644 index 47f9c1c8700..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/ThreadingOptions.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import io.netty.util.concurrent.DefaultThreadFactory; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * A set of hooks that allow clients to customize the driver's internal executors. - * - *

The methods in this class are invoked when the cluster initializes. To customize the behavior, - * extend the class and override the appropriate methods. - * - *

This is mainly intended to allow customization and instrumentation of driver threads. Each - * method must return a newly-allocated executor; don't use a shared executor, as this could - * introduce unintended consequences like deadlocks (we're working to simplify the driver's - * architecture and reduce the number of executors in a future release). The default implementations - * use unbounded queues, which is appropriate when the driver is properly configured; the only - * reason you would want to use bounded queues is to limit memory consumption in case of a bug or - * bad configuration. In that case, make sure to use a {@link RejectedExecutionHandler} that throws, - * such as {@link java.util.concurrent.ThreadPoolExecutor.AbortPolicy}; a blocking handler could - * introduce deadlocks. - * - *

Netty uses a separate pool for I/O operations, that can be configured via {@link - * NettyOptions}. - */ -public class ThreadingOptions { - // Kept for backward compatibility, but this should be customized via this class now - private static final int NON_BLOCKING_EXECUTOR_SIZE = - SystemProperties.getInt( - "com.datastax.driver.NON_BLOCKING_EXECUTOR_SIZE", - Runtime.getRuntime().availableProcessors()); - private static final int DEFAULT_THREAD_KEEP_ALIVE_SECONDS = 30; - - /** - * Builds a thread factory for the threads created by a given executor. - * - *

This is used by the default implementations in this class, and also internally to create the - * Netty I/O pool. - * - * @param clusterName the name of the cluster, as specified by {@link - * com.datastax.driver.core.Cluster.Builder#withClusterName(String)}. - * @param executorName a name that identifies the executor. - * @return the thread factory. - */ - public ThreadFactory createThreadFactory(String clusterName, String executorName) { - return new ThreadFactoryBuilder() - .setNameFormat(clusterName + "-" + executorName + "-%d") - // Back with Netty's thread factory in order to create FastThreadLocalThread instances. This - // allows - // an optimization around ThreadLocals (we could use DefaultThreadFactory directly but it - // creates - // slightly different thread names, so keep we keep a ThreadFactoryBuilder wrapper for - // backward - // compatibility). - .setThreadFactory(new DefaultThreadFactory("ignored name")) - .build(); - } - - /** - * Builds the main internal executor, used for tasks such as scheduling speculative executions, - * triggering registered {@link SchemaChangeListener}s, reacting to node state changes, and - * metadata updates. - * - *

The default implementation sets the pool size to the number of available cores. - * - * @param clusterName the name of the cluster, as specified by {@link - * com.datastax.driver.core.Cluster.Builder#withClusterName(String)}. - * @return the executor. - */ - public ExecutorService createExecutor(String clusterName) { - ThreadPoolExecutor executor = - new ThreadPoolExecutor( - NON_BLOCKING_EXECUTOR_SIZE, - NON_BLOCKING_EXECUTOR_SIZE, - DEFAULT_THREAD_KEEP_ALIVE_SECONDS, - TimeUnit.SECONDS, - new LinkedBlockingQueue(), - createThreadFactory(clusterName, "worker")); - executor.allowCoreThreadTimeOut(true); - return executor; - } - - /** - * Builds the executor used to block on new connections before they are added to a pool. - * - *

The default implementation uses 2 threads. - * - * @param clusterName the name of the cluster, as specified by {@link - * com.datastax.driver.core.Cluster.Builder#withClusterName(String)}. - * @return the executor. - */ - public ExecutorService createBlockingExecutor(String clusterName) { - ThreadPoolExecutor executor = - new ThreadPoolExecutor( - 2, - 2, - DEFAULT_THREAD_KEEP_ALIVE_SECONDS, - TimeUnit.SECONDS, - new LinkedBlockingQueue(), - createThreadFactory(clusterName, "blocking-task-worker")); - executor.allowCoreThreadTimeOut(true); - return executor; - } - - /** - * Builds the executor when reconnection attempts will be scheduled. - * - *

The default implementation uses 2 threads. - * - * @param clusterName the name of the cluster, as specified by {@link - * com.datastax.driver.core.Cluster.Builder#withClusterName(String)}. - * @return the executor. - */ - public ScheduledExecutorService createReconnectionExecutor(String clusterName) { - return new ScheduledThreadPoolExecutor(2, createThreadFactory(clusterName, "reconnection")); - } - - /** - * Builds the executor to handle host state notifications from Cassandra. - * - *

This executor must have exactly one thread so that notifications are processed in - * order. - * - * @param clusterName the name of the cluster, as specified by {@link - * com.datastax.driver.core.Cluster.Builder#withClusterName(String)}. - * @return the executor. - */ - public ScheduledExecutorService createScheduledTasksExecutor(String clusterName) { - return new ScheduledThreadPoolExecutor( - 1, createThreadFactory(clusterName, "scheduled-task-worker")); - } - - /** - * Builds the executor for an internal maintenance task used to clean up closed connections. - * - *

A single scheduled task runs on this executor, so there is no reason to use more than one - * thread. - * - * @param clusterName the name of the cluster, as specified by {@link - * com.datastax.driver.core.Cluster.Builder#withClusterName(String)}. - * @return the executor. - */ - public ScheduledExecutorService createReaperExecutor(String clusterName) { - return new ScheduledThreadPoolExecutor( - 1, createThreadFactory(clusterName, "connection-reaper")); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/TimestampGenerator.java b/driver-core/src/main/java/com/datastax/driver/core/TimestampGenerator.java deleted file mode 100644 index 242ec4aa2bd..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/TimestampGenerator.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * Generates client-side, microsecond-precision query timestamps. - * - *

Given that Cassandra uses those timestamps to resolve conflicts, implementations should - * generate monotonically increasing timestamps for successive invocations of {@link #next()}. - */ -public interface TimestampGenerator { - - /** - * Returns the next timestamp. - * - *

Implementors should enforce increasing monotonicity of timestamps, that is, a timestamp - * returned should always be strictly greater that any previously returned timestamp. - * - *

Implementors should strive to achieve microsecond precision in the best possible way, which - * is usually largely dependent on the underlying operating system's capabilities. - * - * @return the next timestamp (in microseconds). If it equals {@link Long#MIN_VALUE}, it won't be - * sent by the driver, letting Cassandra generate a server-side timestamp. - */ - long next(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/Token.java b/driver-core/src/main/java/com/datastax/driver/core/Token.java deleted file mode 100644 index a3a1000d6c6..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/Token.java +++ /dev/null @@ -1,674 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.Bytes; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Lists; -import com.google.common.primitives.UnsignedBytes; -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.List; - -/** A token on the Cassandra ring. */ -public abstract class Token implements Comparable { - - /** - * Returns the data type of this token's value. - * - * @return the datatype. - */ - public abstract DataType getType(); - - /** - * Returns the raw value of this token. - * - * @return the value. - */ - public abstract Object getValue(); - - /** - * Returns the serialized form of the current token, using the appropriate codec depending on the - * partitioner in use and the CQL datatype for the token. - * - * @param protocolVersion the protocol version in use. - * @return the serialized form of the current token - */ - public abstract ByteBuffer serialize(ProtocolVersion protocolVersion); - - static Token.Factory getFactory(String partitionerName) { - if (partitionerName.endsWith("Murmur3Partitioner")) return M3PToken.FACTORY; - else if (partitionerName.endsWith("RandomPartitioner")) return RPToken.FACTORY; - else if (partitionerName.endsWith("OrderedPartitioner")) return OPPToken.FACTORY; - else return null; - } - - abstract static class Factory { - abstract Token fromString(String tokenStr); - - abstract DataType getTokenType(); - - abstract Token deserialize(ByteBuffer buffer, ProtocolVersion protocolVersion); - - /** - * The minimum token is a special value that no key ever hashes to, it's used both as lower and - * upper bound. - */ - abstract Token minToken(); - - abstract Token hash(ByteBuffer partitionKey); - - abstract List split(Token startToken, Token endToken, int numberOfSplits); - - // Base implementation for split - protected List split( - BigInteger start, - BigInteger range, - BigInteger ringEnd, - BigInteger ringLength, - int numberOfSplits) { - BigInteger[] tmp = range.divideAndRemainder(BigInteger.valueOf(numberOfSplits)); - BigInteger divider = tmp[0]; - int remainder = tmp[1].intValue(); - - List results = Lists.newArrayListWithExpectedSize(numberOfSplits - 1); - BigInteger current = start; - BigInteger dividerPlusOne = - (remainder == 0) - ? null // won't be used - : divider.add(BigInteger.ONE); - - for (int i = 1; i < numberOfSplits; i++) { - current = current.add(remainder-- > 0 ? dividerPlusOne : divider); - if (ringEnd != null && current.compareTo(ringEnd) > 0) - current = current.subtract(ringLength); - results.add(current); - } - return results; - } - } - - // Murmur3Partitioner tokens - static class M3PToken extends Token { - private final long value; - - public static final Factory FACTORY = new M3PTokenFactory(); - - private static class M3PTokenFactory extends Factory { - - private static final BigInteger RING_END = BigInteger.valueOf(Long.MAX_VALUE); - private static final BigInteger RING_LENGTH = - RING_END.subtract(BigInteger.valueOf(Long.MIN_VALUE)); - static final M3PToken MIN_TOKEN = new M3PToken(Long.MIN_VALUE); - static final M3PToken MAX_TOKEN = new M3PToken(Long.MAX_VALUE); - - private long getblock(ByteBuffer key, int offset, int index) { - int i_8 = index << 3; - int blockOffset = offset + i_8; - return ((long) key.get(blockOffset + 0) & 0xff) - + (((long) key.get(blockOffset + 1) & 0xff) << 8) - + (((long) key.get(blockOffset + 2) & 0xff) << 16) - + (((long) key.get(blockOffset + 3) & 0xff) << 24) - + (((long) key.get(blockOffset + 4) & 0xff) << 32) - + (((long) key.get(blockOffset + 5) & 0xff) << 40) - + (((long) key.get(blockOffset + 6) & 0xff) << 48) - + (((long) key.get(blockOffset + 7) & 0xff) << 56); - } - - private long rotl64(long v, int n) { - return ((v << n) | (v >>> (64 - n))); - } - - private long fmix(long k) { - k ^= k >>> 33; - k *= 0xff51afd7ed558ccdL; - k ^= k >>> 33; - k *= 0xc4ceb9fe1a85ec53L; - k ^= k >>> 33; - return k; - } - - // This is an adapted version of the MurmurHash.hash3_x64_128 from Cassandra used - // for M3P. Compared to that methods, there's a few inlining of arguments and we - // only return the first 64-bits of the result since that's all M3P uses. - @SuppressWarnings("fallthrough") - private long murmur(ByteBuffer data) { - int offset = data.position(); - int length = data.remaining(); - - int nblocks = length >> 4; // Process as 128-bit blocks. - - long h1 = 0; - long h2 = 0; - - long c1 = 0x87c37b91114253d5L; - long c2 = 0x4cf5ad432745937fL; - - // ---------- - // body - - for (int i = 0; i < nblocks; i++) { - long k1 = getblock(data, offset, i * 2 + 0); - long k2 = getblock(data, offset, i * 2 + 1); - - k1 *= c1; - k1 = rotl64(k1, 31); - k1 *= c2; - h1 ^= k1; - h1 = rotl64(h1, 27); - h1 += h2; - h1 = h1 * 5 + 0x52dce729; - k2 *= c2; - k2 = rotl64(k2, 33); - k2 *= c1; - h2 ^= k2; - h2 = rotl64(h2, 31); - h2 += h1; - h2 = h2 * 5 + 0x38495ab5; - } - - // ---------- - // tail - - // Advance offset to the unprocessed tail of the data. - offset += nblocks * 16; - - long k1 = 0; - long k2 = 0; - - switch (length & 15) { - case 15: - k2 ^= ((long) data.get(offset + 14)) << 48; - case 14: - k2 ^= ((long) data.get(offset + 13)) << 40; - case 13: - k2 ^= ((long) data.get(offset + 12)) << 32; - case 12: - k2 ^= ((long) data.get(offset + 11)) << 24; - case 11: - k2 ^= ((long) data.get(offset + 10)) << 16; - case 10: - k2 ^= ((long) data.get(offset + 9)) << 8; - case 9: - k2 ^= ((long) data.get(offset + 8)) << 0; - k2 *= c2; - k2 = rotl64(k2, 33); - k2 *= c1; - h2 ^= k2; - - case 8: - k1 ^= ((long) data.get(offset + 7)) << 56; - case 7: - k1 ^= ((long) data.get(offset + 6)) << 48; - case 6: - k1 ^= ((long) data.get(offset + 5)) << 40; - case 5: - k1 ^= ((long) data.get(offset + 4)) << 32; - case 4: - k1 ^= ((long) data.get(offset + 3)) << 24; - case 3: - k1 ^= ((long) data.get(offset + 2)) << 16; - case 2: - k1 ^= ((long) data.get(offset + 1)) << 8; - case 1: - k1 ^= ((long) data.get(offset)); - k1 *= c1; - k1 = rotl64(k1, 31); - k1 *= c2; - h1 ^= k1; - } - - // ---------- - // finalization - - h1 ^= length; - h2 ^= length; - - h1 += h2; - h2 += h1; - - h1 = fmix(h1); - h2 = fmix(h2); - - h1 += h2; - h2 += h1; - - return h1; - } - - @Override - M3PToken fromString(String tokenStr) { - return new M3PToken(Long.parseLong(tokenStr)); - } - - @Override - DataType getTokenType() { - return DataType.bigint(); - } - - @Override - Token deserialize(ByteBuffer buffer, ProtocolVersion protocolVersion) { - return new M3PToken(TypeCodec.bigint().deserialize(buffer, protocolVersion)); - } - - @Override - Token minToken() { - return MIN_TOKEN; - } - - @Override - M3PToken hash(ByteBuffer partitionKey) { - long v = murmur(partitionKey); - return new M3PToken(v == Long.MIN_VALUE ? Long.MAX_VALUE : v); - } - - @Override - List split(Token startToken, Token endToken, int numberOfSplits) { - // edge case: ]min, min] means the whole ring - if (startToken.equals(endToken) && startToken.equals(MIN_TOKEN)) endToken = MAX_TOKEN; - - BigInteger start = BigInteger.valueOf(((M3PToken) startToken).value); - BigInteger end = BigInteger.valueOf(((M3PToken) endToken).value); - - BigInteger range = end.subtract(start); - if (range.compareTo(BigInteger.ZERO) < 0) range = range.add(RING_LENGTH); - - List values = super.split(start, range, RING_END, RING_LENGTH, numberOfSplits); - List tokens = Lists.newArrayListWithExpectedSize(values.size()); - for (BigInteger value : values) tokens.add(new M3PToken(value.longValue())); - return tokens; - } - } - - private M3PToken(long value) { - this.value = value; - } - - @Override - public DataType getType() { - return FACTORY.getTokenType(); - } - - @Override - public Object getValue() { - return value; - } - - @Override - public ByteBuffer serialize(ProtocolVersion protocolVersion) { - return TypeCodec.bigint().serialize(value, protocolVersion); - } - - @Override - public int compareTo(Token other) { - assert other instanceof M3PToken; - long otherValue = ((M3PToken) other).value; - return value < otherValue ? -1 : (value == otherValue) ? 0 : 1; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || this.getClass() != obj.getClass()) return false; - - return value == ((M3PToken) obj).value; - } - - @Override - public int hashCode() { - return (int) (value ^ (value >>> 32)); - } - - @Override - public String toString() { - return Long.toString(value); - } - } - - // OPPartitioner tokens - static class OPPToken extends Token { - - private final ByteBuffer value; - - public static final Factory FACTORY = new OPPTokenFactory(); - - private static class OPPTokenFactory extends Factory { - private static final BigInteger TWO = BigInteger.valueOf(2); - private static final Token MIN_TOKEN = new OPPToken(ByteBuffer.allocate(0)); - - @Override - public OPPToken fromString(String tokenStr) { - // This method must be able to parse the contents of system.peers.tokens, which do not have - // the "0x" prefix. - // On the other hand, OPPToken#toString has the "0x" because it should be usable in a CQL - // query, and it's - // nice to have fromString and toString symetrical. - // So handle both cases: - if (!tokenStr.startsWith("0x")) { - String prefix = (tokenStr.length() % 2 == 0) ? "0x" : "0x0"; - tokenStr = prefix + tokenStr; - } - ByteBuffer value = Bytes.fromHexString(tokenStr); - return new OPPToken(value); - } - - @Override - DataType getTokenType() { - return DataType.blob(); - } - - @Override - Token deserialize(ByteBuffer buffer, ProtocolVersion protocolVersion) { - return new OPPToken(buffer); - } - - @Override - Token minToken() { - return MIN_TOKEN; - } - - @Override - OPPToken hash(ByteBuffer partitionKey) { - return new OPPToken(partitionKey); - } - - @Override - List split(Token startToken, Token endToken, int numberOfSplits) { - int tokenOrder = startToken.compareTo(endToken); - - // ]min,min] means the whole ring. However, since there is no "max token" with this - // partitioner, we can't come up - // with a magic end value that would cover the whole ring - if (tokenOrder == 0 && startToken.equals(MIN_TOKEN)) - throw new IllegalArgumentException("Cannot split whole ring with ordered partitioner"); - - OPPToken oppStartToken = (OPPToken) startToken; - OPPToken oppEndToken = (OPPToken) endToken; - - int significantBytes; - BigInteger start, end, range, ringEnd, ringLength; - BigInteger bigNumberOfSplits = BigInteger.valueOf(numberOfSplits); - if (tokenOrder < 0) { - // Since tokens are compared lexicographically, convert to integers using the largest - // length - // (ex: given 0x0A and 0x0BCD, switch to 0x0A00 and 0x0BCD) - significantBytes = Math.max(oppStartToken.value.capacity(), oppEndToken.value.capacity()); - - // If the number of splits does not fit in the difference between the two integers, use - // more bytes - // (ex: cannot fit 4 splits between 0x01 and 0x03, so switch to 0x0100 and 0x0300) - // At most 4 additional bytes will be needed, since numberOfSplits is an integer. - int addedBytes = 0; - while (true) { - start = toBigInteger(oppStartToken.value, significantBytes); - end = toBigInteger(oppEndToken.value, significantBytes); - range = end.subtract(start); - if (addedBytes == 4 || start.equals(end) || range.compareTo(bigNumberOfSplits) >= 0) - break; - significantBytes += 1; - addedBytes += 1; - } - ringEnd = ringLength = null; // won't be used - } else { - // Same logic except that we wrap around the ring - significantBytes = Math.max(oppStartToken.value.capacity(), oppEndToken.value.capacity()); - int addedBytes = 0; - while (true) { - start = toBigInteger(oppStartToken.value, significantBytes); - end = toBigInteger(oppEndToken.value, significantBytes); - ringLength = TWO.pow(significantBytes * 8); - ringEnd = ringLength.subtract(BigInteger.ONE); - range = end.subtract(start).add(ringLength); - if (addedBytes == 4 || range.compareTo(bigNumberOfSplits) >= 0) break; - significantBytes += 1; - addedBytes += 1; - } - } - - List values = super.split(start, range, ringEnd, ringLength, numberOfSplits); - List tokens = Lists.newArrayListWithExpectedSize(values.size()); - for (BigInteger value : values) tokens.add(new OPPToken(toBytes(value, significantBytes))); - return tokens; - } - - // Convert a token's byte array to a number in order to perform computations. - // This depends on the number of "significant bytes" that we use to normalize all tokens to - // the same size. - // For example if the token is 0x01 but significantBytes is 2, the result is 8 (0x0100). - private BigInteger toBigInteger(ByteBuffer bb, int significantBytes) { - byte[] bytes = Bytes.getArray(bb); - byte[] target; - if (significantBytes != bytes.length) { - target = new byte[significantBytes]; - System.arraycopy(bytes, 0, target, 0, bytes.length); - } else target = bytes; - return new BigInteger(1, target); - } - - // Convert a numeric representation back to a byte array. - // Again, the number of significant bytes matters: if the input value is 1 but - // significantBytes is 2, the - // expected result is 0x0001 (a simple conversion would produce 0x01). - protected ByteBuffer toBytes(BigInteger value, int significantBytes) { - byte[] rawBytes = value.toByteArray(); - byte[] result; - if (rawBytes.length == significantBytes) result = rawBytes; - else { - result = new byte[significantBytes]; - int start, length; - if (rawBytes[0] == 0) { // that's a sign byte, ignore (it can cause rawBytes.length == - // significantBytes + 1) - start = 1; - length = rawBytes.length - 1; - } else { - start = 0; - length = rawBytes.length; - } - System.arraycopy(rawBytes, start, result, significantBytes - length, length); - } - return ByteBuffer.wrap(result); - } - } - - @VisibleForTesting - OPPToken(ByteBuffer value) { - this.value = value; - } - - @Override - public DataType getType() { - return FACTORY.getTokenType(); - } - - @Override - public Object getValue() { - return value; - } - - @Override - public ByteBuffer serialize(ProtocolVersion protocolVersion) { - return TypeCodec.blob().serialize(value, protocolVersion); - } - - @Override - public int compareTo(Token other) { - assert other instanceof OPPToken; - return UnsignedBytes.lexicographicalComparator() - .compare(Bytes.getArray(value), Bytes.getArray(((OPPToken) other).value)); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || this.getClass() != obj.getClass()) return false; - - return value.equals(((OPPToken) obj).value); - } - - @Override - public int hashCode() { - return value.hashCode(); - } - - @Override - public String toString() { - return Bytes.toHexString(value); - } - } - - // RandomPartitioner tokens - static class RPToken extends Token { - - private final BigInteger value; - - public static final Factory FACTORY = new RPTokenFactory(); - - private static class RPTokenFactory extends Factory { - - private static final BigInteger MIN_VALUE = BigInteger.ONE.negate(); - private static final BigInteger MAX_VALUE = BigInteger.valueOf(2).pow(127); - private static final BigInteger RING_LENGTH = MAX_VALUE.add(BigInteger.ONE); - private static final Token MIN_TOKEN = new RPToken(MIN_VALUE); - private static final Token MAX_TOKEN = new RPToken(MAX_VALUE); - - private final MessageDigest prototype; - private final boolean supportsClone; - - private RPTokenFactory() { - prototype = createMessageDigest(); - boolean supportsClone; - try { - prototype.clone(); - supportsClone = true; - } catch (CloneNotSupportedException e) { - supportsClone = false; - } - this.supportsClone = supportsClone; - } - - private static MessageDigest createMessageDigest() { - try { - return MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("MD5 doesn't seem to be available on this JVM", e); - } - } - - private MessageDigest newMessageDigest() { - if (supportsClone) { - try { - return (MessageDigest) prototype.clone(); - } catch (CloneNotSupportedException ignored) { - } - } - return createMessageDigest(); - } - - private BigInteger md5(ByteBuffer data) { - MessageDigest digest = newMessageDigest(); - digest.update(data.duplicate()); - return new BigInteger(digest.digest()).abs(); - } - - @Override - RPToken fromString(String tokenStr) { - return new RPToken(new BigInteger(tokenStr)); - } - - @Override - DataType getTokenType() { - return DataType.varint(); - } - - @Override - Token deserialize(ByteBuffer buffer, ProtocolVersion protocolVersion) { - return new RPToken(TypeCodec.varint().deserialize(buffer, protocolVersion)); - } - - @Override - Token minToken() { - return MIN_TOKEN; - } - - @Override - RPToken hash(ByteBuffer partitionKey) { - return new RPToken(md5(partitionKey)); - } - - @Override - List split(Token startToken, Token endToken, int numberOfSplits) { - // edge case: ]min, min] means the whole ring - if (startToken.equals(endToken) && startToken.equals(MIN_TOKEN)) endToken = MAX_TOKEN; - - BigInteger start = ((RPToken) startToken).value; - BigInteger end = ((RPToken) endToken).value; - - BigInteger range = end.subtract(start); - if (range.compareTo(BigInteger.ZERO) < 0) range = range.add(RING_LENGTH); - - List values = super.split(start, range, MAX_VALUE, RING_LENGTH, numberOfSplits); - List tokens = Lists.newArrayListWithExpectedSize(values.size()); - for (BigInteger value : values) tokens.add(new RPToken(value)); - return tokens; - } - } - - private RPToken(BigInteger value) { - this.value = value; - } - - @Override - public DataType getType() { - return FACTORY.getTokenType(); - } - - @Override - public Object getValue() { - return value; - } - - @Override - public ByteBuffer serialize(ProtocolVersion protocolVersion) { - return TypeCodec.varint().serialize(value, protocolVersion); - } - - @Override - public int compareTo(Token other) { - assert other instanceof RPToken; - return value.compareTo(((RPToken) other).value); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null || this.getClass() != obj.getClass()) return false; - - return value.equals(((RPToken) obj).value); - } - - @Override - public int hashCode() { - return value.hashCode(); - } - - @Override - public String toString() { - return value.toString(); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/TokenRange.java b/driver-core/src/main/java/com/datastax/driver/core/TokenRange.java deleted file mode 100644 index 3b62c1fedc6..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/TokenRange.java +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.MoreObjects; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import java.util.ArrayList; -import java.util.List; - -/** - * A range of tokens on the Cassandra ring. - * - *

A range is start-exclusive and end-inclusive. It is empty when start and end are the same - * token, except if that is the minimum token, in which case the range covers the whole ring (this - * is consistent with the behavior of CQL range queries). - * - *

Note that CQL does not handle wrapping. To query all partitions in a range, see {@link - * #unwrap()}. - */ -public final class TokenRange implements Comparable { - private final Token start; - private final Token end; - @VisibleForTesting final Token.Factory factory; - - TokenRange(Token start, Token end, Token.Factory factory) { - this.start = start; - this.end = end; - this.factory = factory; - } - - /** - * Return the start of the range. - * - * @return the start of the range (exclusive). - */ - public Token getStart() { - return start; - } - - /** - * Return the end of the range. - * - * @return the end of the range (inclusive). - */ - public Token getEnd() { - return end; - } - - /** - * Splits this range into a number of smaller ranges of equal "size" (referring to the number of - * tokens, not the actual amount of data). - * - *

Splitting an empty range is not permitted. But note that, in edge cases, splitting a range - * might produce one or more empty ranges. - * - * @param numberOfSplits the number of splits to create. - * @return the splits. - * @throws IllegalArgumentException if the range is empty or if numberOfSplits < 1. - */ - public List splitEvenly(int numberOfSplits) { - if (numberOfSplits < 1) - throw new IllegalArgumentException( - String.format("numberOfSplits (%d) must be greater than 0.", numberOfSplits)); - if (isEmpty()) throw new IllegalArgumentException("Can't split empty range " + this); - - List tokenRanges = new ArrayList(); - List splitPoints = factory.split(start, end, numberOfSplits); - Token splitStart = start; - for (Token splitEnd : splitPoints) { - tokenRanges.add(new TokenRange(splitStart, splitEnd, factory)); - splitStart = splitEnd; - } - tokenRanges.add(new TokenRange(splitStart, end, factory)); - return tokenRanges; - } - - /** - * Returns whether this range is empty. - * - *

A range is empty when start and end are the same token, except if that is the minimum token, - * in which case the range covers the whole ring (this is consistent with the behavior of CQL - * range queries). - * - * @return whether the range is empty. - */ - public boolean isEmpty() { - return start.equals(end) && !start.equals(factory.minToken()); - } - - /** - * Returns whether this range wraps around the end of the ring. - * - * @return whether this range wraps around. - */ - public boolean isWrappedAround() { - return start.compareTo(end) > 0 && !end.equals(factory.minToken()); - } - - /** - * Splits this range into a list of two non-wrapping ranges. This will return the range itself if - * it is non-wrapping, or two ranges otherwise. - * - *

For example: - * - *

    - *
  • {@code ]1,10]} unwraps to itself; - *
  • {@code ]10,1]} unwraps to {@code ]10,min_token]} and {@code ]min_token,1]}. - *
- * - *

This is useful for CQL range queries, which do not handle wrapping: - * - *

{@code
-   * List rows = new ArrayList();
-   * for (TokenRange subRange : range.unwrap()) {
-   *     ResultSet rs = session.execute("SELECT * FROM mytable WHERE token(pk) > ? and token(pk) <= ?",
-   *                                    subRange.getStart(), subRange.getEnd());
-   *     rows.addAll(rs.all());
-   * }
-   * }
- * - * @return the list of non-wrapping ranges. - */ - public List unwrap() { - if (isWrappedAround()) { - return ImmutableList.of( - new TokenRange(start, factory.minToken(), factory), - new TokenRange(factory.minToken(), end, factory)); - } else { - return ImmutableList.of(this); - } - } - - /** - * Returns whether this range intersects another one. - * - *

For example: - * - *

    - *
  • {@code ]3,5]} intersects {@code ]1,4]}, {@code ]4,5]}... - *
  • {@code ]3,5]} does not intersect {@code ]1,2]}, {@code ]2,3]}, {@code ]5,7]}... - *
- * - * @param that the other range. - * @return whether they intersect. - */ - public boolean intersects(TokenRange that) { - // Empty ranges never intersect any other range - if (this.isEmpty() || that.isEmpty()) return false; - - return this.contains(that.start, true) - || this.contains(that.end, false) - || that.contains(this.start, true) - || that.contains(this.end, false); - } - - /** - * Computes the intersection of this range with another one. - * - *

If either of these ranges overlap the the ring, they are unwrapped and the unwrapped tokens - * are compared with one another. - * - *

This call will fail if the two ranges do not intersect, you must check by calling {@link - * #intersects(TokenRange)} beforehand. - * - * @param that the other range. - * @return the range(s) resulting from the intersection. - * @throws IllegalArgumentException if the ranges do not intersect. - */ - public List intersectWith(TokenRange that) { - if (!this.intersects(that)) - throw new IllegalArgumentException( - "The two ranges do not intersect, use intersects() before calling this method"); - - List intersected = Lists.newArrayList(); - - // Compare the unwrapped ranges to one another. - List unwrappedForThis = this.unwrap(); - List unwrappedForThat = that.unwrap(); - for (TokenRange t1 : unwrappedForThis) { - for (TokenRange t2 : unwrappedForThat) { - if (t1.intersects(t2)) { - intersected.add( - new TokenRange( - (t1.contains(t2.start, true)) ? t2.start : t1.start, - (t1.contains(t2.end, false)) ? t2.end : t1.end, - factory)); - } - } - } - - // If two intersecting ranges were produced, merge them if they are adjacent. - // This could happen in the case that two wrapped ranges intersected. - if (intersected.size() == 2) { - TokenRange t1 = intersected.get(0); - TokenRange t2 = intersected.get(1); - if (t1.end.equals(t2.start) || t2.end.equals(t1.start)) { - return ImmutableList.of(t1.mergeWith(t2)); - } - } - - return intersected; - } - - /** - * Checks whether this range contains a given token. - * - * @param token the token to check for. - * @return whether this range contains the token, i.e. {@code range.start < token <= range.end}. - */ - public boolean contains(Token token) { - return contains(token, false); - } - - // isStart handles the case where the token is the start of another range, for example: - // * ]1,2] contains 2, but it does not contain the start of ]2,3] - // * ]1,2] does not contain 1, but it contains the start of ]1,3] - @VisibleForTesting - boolean contains(Token token, boolean isStart) { - if (isEmpty()) { - return false; - } - Token minToken = factory.minToken(); - if (end.equals(minToken)) { - if (start.equals(minToken)) { // ]min, min] = full ring, contains everything - return true; - } else if (token.equals(minToken)) { - return !isStart; - } else { - return isStart ? token.compareTo(start) >= 0 : token.compareTo(start) > 0; - } - } else { - boolean isAfterStart = isStart ? token.compareTo(start) >= 0 : token.compareTo(start) > 0; - boolean isBeforeEnd = isStart ? token.compareTo(end) < 0 : token.compareTo(end) <= 0; - return isWrappedAround() - ? isAfterStart || isBeforeEnd // ####]----]#### - : isAfterStart && isBeforeEnd; // ----]####]---- - } - } - - /** - * Merges this range with another one. - * - *

The two ranges should either intersect or be adjacent; in other words, the merged range - * should not include tokens that are in neither of the original ranges. - * - *

For example: - * - *

    - *
  • merging {@code ]3,5]} with {@code ]4,7]} produces {@code ]3,7]}; - *
  • merging {@code ]3,5]} with {@code ]4,5]} produces {@code ]3,5]}; - *
  • merging {@code ]3,5]} with {@code ]5,8]} produces {@code ]3,8]}; - *
  • merging {@code ]3,5]} with {@code ]6,8]} fails. - *
- * - * @param that the other range. - * @return the resulting range. - * @throws IllegalArgumentException if the ranges neither intersect nor are adjacent. - */ - public TokenRange mergeWith(TokenRange that) { - if (this.equals(that)) return this; - - if (!(this.intersects(that) || this.end.equals(that.start) || that.end.equals(this.start))) - throw new IllegalArgumentException( - String.format( - "Can't merge %s with %s because they neither intersect nor are adjacent", - this, that)); - - if (this.isEmpty()) return that; - - if (that.isEmpty()) return this; - - // That's actually "starts in or is adjacent to the end of" - boolean thisStartsInThat = that.contains(this.start, true) || this.start.equals(that.end); - boolean thatStartsInThis = this.contains(that.start, true) || that.start.equals(this.end); - - // This takes care of all the cases that return the full ring, so that we don't have to worry - // about them below - if (thisStartsInThat && thatStartsInThis) return fullRing(); - - // Starting at this.start, see how far we can go while staying in at least one of the ranges. - Token mergedEnd = (thatStartsInThis && !this.contains(that.end, false)) ? that.end : this.end; - - // Repeat in the other direction. - Token mergedStart = thisStartsInThat ? that.start : this.start; - - return new TokenRange(mergedStart, mergedEnd, factory); - } - - private TokenRange fullRing() { - return new TokenRange(factory.minToken(), factory.minToken(), factory); - } - - @Override - public boolean equals(Object other) { - if (other == this) return true; - if (other instanceof TokenRange) { - TokenRange that = (TokenRange) other; - return MoreObjects.equal(this.start, that.start) && MoreObjects.equal(this.end, that.end); - } - return false; - } - - @Override - public int hashCode() { - return MoreObjects.hashCode(start, end); - } - - @Override - public String toString() { - return String.format("]%s, %s]", start, end); - } - - @Override - public int compareTo(TokenRange other) { - if (this.equals(other)) { - return 0; - } else { - int compareStart = this.start.compareTo(other.start); - return compareStart != 0 ? compareStart : this.end.compareTo(other.end); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/TranslatedAddressEndPoint.java b/driver-core/src/main/java/com/datastax/driver/core/TranslatedAddressEndPoint.java deleted file mode 100644 index 54848fd9982..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/TranslatedAddressEndPoint.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.base.Objects; -import java.net.InetSocketAddress; - -/** - * An endpoint based on server-reported RPC addresses, that might require translation if they are - * accessed through a proxy. - */ -class TranslatedAddressEndPoint implements EndPoint { - - private final InetSocketAddress translatedAddress; - - TranslatedAddressEndPoint(InetSocketAddress translatedAddress) { - this.translatedAddress = translatedAddress; - } - - @Override - public InetSocketAddress resolve() { - return translatedAddress; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } else if (other instanceof TranslatedAddressEndPoint) { - TranslatedAddressEndPoint that = (TranslatedAddressEndPoint) other; - return Objects.equal(this.translatedAddress, that.translatedAddress); - } else { - return false; - } - } - - @Override - public int hashCode() { - return translatedAddress.hashCode(); - } - - @Override - public String toString() { - return translatedAddress.toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/TupleType.java b/driver-core/src/main/java/com/datastax/driver/core/TupleType.java deleted file mode 100644 index eb4e7bc118f..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/TupleType.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.exceptions.InvalidTypeException; -import com.google.common.collect.ImmutableList; -import java.util.Arrays; -import java.util.List; - -/** - * A tuple type. - * - *

A tuple type is a essentially a list of types. - */ -public class TupleType extends DataType { - - private final List types; - private final ProtocolVersion protocolVersion; - private volatile CodecRegistry codecRegistry; - - TupleType(List types, ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - super(DataType.Name.TUPLE); - this.types = ImmutableList.copyOf(types); - this.protocolVersion = protocolVersion; - this.codecRegistry = codecRegistry; - } - - /** - * Creates a "disconnected" tuple type (you should prefer {@link - * Metadata#newTupleType(DataType...) cluster.getMetadata().newTupleType(...)} whenever - * possible). - * - *

This method is only exposed for situations where you don't have a {@code Cluster} instance - * available. If you create a type with this method and use it with a {@code Cluster} later, you - * won't be able to set tuple fields with custom codecs registered against the cluster, or you - * might get errors if the protocol versions don't match. - * - * @param protocolVersion the protocol version to use. - * @param codecRegistry the codec registry to use. - * @param types the types for the tuple type. - * @return the newly created tuple type. - */ - public static TupleType of( - ProtocolVersion protocolVersion, CodecRegistry codecRegistry, DataType... types) { - return new TupleType(Arrays.asList(types), protocolVersion, codecRegistry); - } - - /** - * The (immutable) list of types composing this tuple type. - * - * @return the (immutable) list of types composing this tuple type. - */ - public List getComponentTypes() { - return types; - } - - /** - * Returns a new empty value for this tuple type. - * - * @return an empty (with all component to {@code null}) value for this user type definition. - */ - public TupleValue newValue() { - return new TupleValue(this); - } - - /** - * Returns a new value for this tuple type that uses the provided values for the components. - * - *

The numbers of values passed to this method must correspond to the number of components in - * this tuple type. The {@code i}th parameter value will then be assigned to the {@code i}th - * component of the resulting tuple value. - * - * @param values the values to use for the component of the resulting tuple. - * @return a new tuple values based on the provided values. - * @throws IllegalArgumentException if the number of {@code values} provided does not correspond - * to the number of components in this tuple type. - * @throws InvalidTypeException if any of the provided value is not of the correct type for the - * component. - */ - public TupleValue newValue(Object... values) { - if (values.length != types.size()) - throw new IllegalArgumentException( - String.format( - "Invalid number of values. Expecting %d but got %d", types.size(), values.length)); - - TupleValue t = newValue(); - for (int i = 0; i < values.length; i++) { - DataType dataType = types.get(i); - if (values[i] == null) t.setValue(i, null); - else - t.setValue( - i, codecRegistry.codecFor(dataType, values[i]).serialize(values[i], protocolVersion)); - } - return t; - } - - @Override - public boolean isFrozen() { - return true; - } - - /** - * Return the protocol version that has been used to deserialize this tuple type, or that will be - * used to serialize it. In most cases this should be the version currently in use by the cluster - * instance that this tuple type belongs to, as reported by {@link - * ProtocolOptions#getProtocolVersion()}. - * - * @return the protocol version that has been used to deserialize this tuple type, or that will be - * used to serialize it. - */ - ProtocolVersion getProtocolVersion() { - return protocolVersion; - } - - CodecRegistry getCodecRegistry() { - return codecRegistry; - } - - void setCodecRegistry(CodecRegistry codecRegistry) { - this.codecRegistry = codecRegistry; - } - - @Override - public int hashCode() { - return Arrays.hashCode(new Object[] {name, types}); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof TupleType)) return false; - - TupleType d = (TupleType) o; - return name == d.name && types.equals(d.types); - } - - /** - * Return {@code true} if this tuple type contains the given tuple type, and {@code false} - * otherwise. - * - *

A tuple type is said to contain another one if the latter has fewer components than the - * former, but all of them are of the same type. E.g. the type {@code tuple} is - * contained by the type {@code tuple}. - * - *

A contained type can be seen as a "partial" view of a containing type, where the missing - * components are supposed to be {@code null}. - * - * @param other the tuple type to compare against the current one - * @return {@code true} if this tuple type contains the given tuple type, and {@code false} - * otherwise. - */ - public boolean contains(TupleType other) { - if (this.equals(other)) return true; - if (other.types.size() > this.types.size()) return false; - return types.subList(0, other.types.size()).equals(other.types); - } - - @Override - public String toString() { - return "frozen<" + asFunctionParameterString() + ">"; - } - - @Override - public String asFunctionParameterString() { - StringBuilder sb = new StringBuilder(); - for (DataType type : types) { - sb.append(sb.length() == 0 ? "tuple<" : ", "); - sb.append(type.asFunctionParameterString()); - } - return sb.append(">").toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/TupleValue.java b/driver-core/src/main/java/com/datastax/driver/core/TupleValue.java deleted file mode 100644 index 952c3453ab4..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/TupleValue.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** A value for a Tuple. */ -public class TupleValue extends AbstractAddressableByIndexData { - - private final TupleType type; - - /** - * Builds a new value for a tuple. - * - * @param type the {@link TupleType} instance defining this tuple's components. - */ - TupleValue(TupleType type) { - super(type.getProtocolVersion(), type.getComponentTypes().size()); - this.type = type; - } - - protected DataType getType(int i) { - return type.getComponentTypes().get(i); - } - - @Override - protected String getName(int i) { - // This is used for error messages - return "component " + i; - } - - @Override - protected CodecRegistry getCodecRegistry() { - return type.getCodecRegistry(); - } - - /** - * The tuple type this is a value of. - * - * @return The tuple type this is a value of. - */ - public TupleType getType() { - return type; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof TupleValue)) return false; - - TupleValue that = (TupleValue) o; - if (!type.equals(that.type)) return false; - - return super.equals(o); - } - - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - TypeCodec codec = getCodecRegistry().codecFor(type); - sb.append(codec.format(this)); - return sb.toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/TypeCodec.java b/driver-core/src/main/java/com/datastax/driver/core/TypeCodec.java deleted file mode 100644 index bbaea77a4ca..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/TypeCodec.java +++ /dev/null @@ -1,2713 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import static com.datastax.driver.core.DataType.CollectionType; -import static com.datastax.driver.core.DataType.Name; -import static com.datastax.driver.core.DataType.smallint; -import static com.datastax.driver.core.DataType.timeuuid; -import static com.datastax.driver.core.DataType.tinyint; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -import com.datastax.driver.core.exceptions.InvalidTypeException; -import com.datastax.driver.core.utils.Bytes; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import com.google.common.reflect.TypeToken; -import java.io.DataInput; -import java.io.IOException; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.BufferUnderflowException; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.regex.Pattern; - -/** - * A Codec that can serialize and deserialize to and from a given {@link #getCqlType() CQL type} and - * a given {@link #getJavaType() Java Type}. - * - *

- * - *

Serializing and deserializing

- * - *

Two methods handle the serialization and deserialization of Java types into CQL types - * according to the native protocol specifications: - * - *

    - *
  1. {@link #serialize(Object, ProtocolVersion)}: used to serialize from the codec's Java type - * to a {@link ByteBuffer} instance corresponding to the codec's CQL type; - *
  2. {@link #deserialize(ByteBuffer, ProtocolVersion)}: used to deserialize a {@link ByteBuffer} - * instance corresponding to the codec's CQL type to the codec's Java type. - *
- * - *

- * - *

Formatting and parsing

- * - *

Two methods handle the formatting and parsing of Java types into CQL strings: - * - *

    - *
  1. {@link #format(Object)}: formats the Java type handled by the codec as a CQL string; - *
  2. {@link #parse(String)}; parses a CQL string into the Java type handled by the codec. - *
- * - *

- * - *

Inspection

- * - *

Codecs also have the following inspection methods: - * - *

- * - *

    - *
  1. {@link #accepts(DataType)}: returns true if the codec can deserialize the given CQL type; - *
  2. {@link #accepts(TypeToken)}: returns true if the codec can serialize the given Java type; - *
  3. {@link #accepts(Object)}; returns true if the codec can serialize the given object. - *
- * - *

- * - *

Implementation notes

- * - *

- * - *

    - *
  1. TypeCodec implementations must be thread-safe. - *
  2. TypeCodec implementations must perform fast and never block. - *
  3. TypeCodec implementations must support all native protocol versions; it is not - * possible to use different codecs for the same types but under different protocol versions. - *
  4. TypeCodec implementations must comply with the native protocol specifications; failing to - * do so will result in unexpected results and could cause the driver to crash. - *
  5. TypeCodec implementations should be stateless and immutable. - *
  6. TypeCodec implementations should interpret {@code null} values and empty - * ByteBuffers (i.e. {@link ByteBuffer#remaining()} == 0) in a - * reasonable way; usually, {@code NULL} CQL values should map to {@code null} - * references, but exceptions exist; e.g. for varchar types, a {@code NULL} CQL value maps to - * a {@code null} reference, whereas an empty buffer maps to an empty String. For collection - * types, it is also admitted that {@code NULL} CQL values map to empty Java collections - * instead of {@code null} references. In any case, the codec's behavior in respect to {@code - * null} values and empty ByteBuffers should be clearly documented. - *
  7. TypeCodec implementations that wish to handle Java primitive types must be - * instantiated with the wrapper Java class instead, and implement the appropriate interface - * (e.g. {@link com.datastax.driver.core.TypeCodec.PrimitiveBooleanCodec} for primitive {@code - * boolean} types; there is one such interface for each Java primitive type). - *
  8. When deserializing, TypeCodec implementations should not consume {@link ByteBuffer} - * instances by performing relative read operations that modify their current position; codecs - * should instead prefer absolute read methods, or, if necessary, they should {@link - * ByteBuffer#duplicate() duplicate} their byte buffers prior to reading them. - *
- * - * @param The codec's Java type - */ -public abstract class TypeCodec { - - /** - * Return the default codec for the CQL type {@code boolean}. The returned codec maps the CQL type - * {@code boolean} into the Java type {@link Boolean}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code boolean}. - */ - public static PrimitiveBooleanCodec cboolean() { - return BooleanCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code tinyint}. The returned codec maps the CQL type - * {@code tinyint} into the Java type {@link Byte}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code tinyint}. - */ - public static PrimitiveByteCodec tinyInt() { - return TinyIntCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code smallint}. The returned codec maps the CQL - * type {@code smallint} into the Java type {@link Short}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code smallint}. - */ - public static PrimitiveShortCodec smallInt() { - return SmallIntCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code int}. The returned codec maps the CQL type - * {@code int} into the Java type {@link Integer}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code int}. - */ - public static PrimitiveIntCodec cint() { - return IntCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code bigint}. The returned codec maps the CQL type - * {@code bigint} into the Java type {@link Long}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code bigint}. - */ - public static PrimitiveLongCodec bigint() { - return BigintCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code counter}. The returned codec maps the CQL type - * {@code counter} into the Java type {@link Long}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code counter}. - */ - public static PrimitiveLongCodec counter() { - return CounterCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code float}. The returned codec maps the CQL type - * {@code float} into the Java type {@link Float}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code float}. - */ - public static PrimitiveFloatCodec cfloat() { - return FloatCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code double}. The returned codec maps the CQL type - * {@code double} into the Java type {@link Double}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code double}. - */ - public static PrimitiveDoubleCodec cdouble() { - return DoubleCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code varint}. The returned codec maps the CQL type - * {@code varint} into the Java type {@link BigInteger}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code varint}. - */ - public static TypeCodec varint() { - return VarintCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code decimal}. The returned codec maps the CQL type - * {@code decimal} into the Java type {@link BigDecimal}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code decimal}. - */ - public static TypeCodec decimal() { - return DecimalCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code ascii}. The returned codec maps the CQL type - * {@code ascii} into the Java type {@link String}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code ascii}. - */ - public static TypeCodec ascii() { - return AsciiCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code varchar}. The returned codec maps the CQL type - * {@code varchar} into the Java type {@link String}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code varchar}. - */ - public static TypeCodec varchar() { - return VarcharCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code blob}. The returned codec maps the CQL type - * {@code blob} into the Java type {@link ByteBuffer}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code blob}. - */ - public static TypeCodec blob() { - return BlobCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code date}. The returned codec maps the CQL type - * {@code date} into the Java type {@link LocalDate}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code date}. - */ - public static TypeCodec date() { - return DateCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code time}. The returned codec maps the CQL type - * {@code time} into the Java type {@link Long}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code time}. - */ - public static PrimitiveLongCodec time() { - return TimeCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code timestamp}. The returned codec maps the CQL - * type {@code timestamp} into the Java type {@link Date}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code timestamp}. - */ - public static TypeCodec timestamp() { - return TimestampCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code uuid}. The returned codec maps the CQL type - * {@code uuid} into the Java type {@link UUID}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code uuid}. - */ - public static TypeCodec uuid() { - return UUIDCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code timeuuid}. The returned codec maps the CQL - * type {@code timeuuid} into the Java type {@link UUID}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code timeuuid}. - */ - public static TypeCodec timeUUID() { - return TimeUUIDCodec.instance; - } - - /** - * Return the default codec for the CQL type {@code inet}. The returned codec maps the CQL type - * {@code inet} into the Java type {@link InetAddress}. The returned instance is a singleton. - * - * @return the default codec for CQL type {@code inet}. - */ - public static TypeCodec inet() { - return InetCodec.instance; - } - - /** - * Return a newly-created codec for the CQL type {@code list} whose element type is determined by - * the given element codec. The returned codec maps the CQL type {@code list} into the Java type - * {@link List}. This method does not cache returned instances and returns a newly-allocated - * object at each invocation. - * - * @param elementCodec the codec that will handle elements of this list. - * @return A newly-created codec for CQL type {@code list}. - */ - public static TypeCodec> list(TypeCodec elementCodec) { - return new ListCodec(elementCodec); - } - - /** - * Return a newly-created codec for the CQL type {@code set} whose element type is determined by - * the given element codec. The returned codec maps the CQL type {@code set} into the Java type - * {@link Set}. This method does not cache returned instances and returns a newly-allocated object - * at each invocation. - * - * @param elementCodec the codec that will handle elements of this set. - * @return A newly-created codec for CQL type {@code set}. - */ - public static TypeCodec> set(TypeCodec elementCodec) { - return new SetCodec(elementCodec); - } - - /** - * Return a newly-created codec for the CQL type {@code map} whose key type and value type are - * determined by the given codecs. The returned codec maps the CQL type {@code map} into the Java - * type {@link Map}. This method does not cache returned instances and returns a newly-allocated - * object at each invocation. - * - * @param keyCodec the codec that will handle keys of this map. - * @param valueCodec the codec that will handle values of this map. - * @return A newly-created codec for CQL type {@code map}. - */ - public static TypeCodec> map(TypeCodec keyCodec, TypeCodec valueCodec) { - return new MapCodec(keyCodec, valueCodec); - } - - /** - * Return a newly-created codec for the given user-defined CQL type. The returned codec maps the - * user-defined type into the Java type {@link UDTValue}. This method does not cache returned - * instances and returns a newly-allocated object at each invocation. - * - * @param type the user-defined type this codec should handle. - * @return A newly-created codec for the given user-defined CQL type. - */ - public static TypeCodec userType(UserType type) { - return new UDTCodec(type); - } - - /** - * Return a newly-created codec for the given CQL tuple type. The returned codec maps the tuple - * type into the Java type {@link TupleValue}. This method does not cache returned instances and - * returns a newly-allocated object at each invocation. - * - * @param type the tuple type this codec should handle. - * @return A newly-created codec for the given CQL tuple type. - */ - public static TypeCodec tuple(TupleType type) { - return new TupleCodec(type); - } - - /** - * Return a newly-created codec for the given CQL custom type. - * - *

The returned codec maps the custom type into the Java type {@link ByteBuffer}, thus - * providing a (very lightweight) support for Cassandra types that do not have a CQL equivalent. - * - *

Note that the returned codec assumes that CQL literals for the given custom type are - * expressed in binary form as well, e.g. {@code 0xcafebabe}. If this is not the case, the - * returned codec might be unable to {@link #parse(String) parse} and {@link #format(Object) - * format} literals for this type. This is notoriously true for types inheriting from {@code - * org.apache.cassandra.db.marshal.AbstractCompositeType}, whose CQL literals are actually - * expressed as quoted strings. - * - *

This method does not cache returned instances and returns a newly-allocated object at each - * invocation. - * - * @param type the custom type this codec should handle. - * @return A newly-created codec for the given CQL custom type. - */ - public static TypeCodec custom(DataType.CustomType type) { - return new CustomCodec(type); - } - - /** - * Returns the default codec for the {@link DataType#duration() Duration type}. - * - *

This codec maps duration types to the driver's built-in {@link Duration} class, thus - * providing a more user-friendly mapping than the low-level mapping provided by regular {@link - * #custom(DataType.CustomType) custom type codecs}. - * - *

The returned instance is a singleton. - * - * @return the default codec for the Duration type. - */ - public static TypeCodec duration() { - return DurationCodec.instance; - } - - protected final TypeToken javaType; - - protected final DataType cqlType; - - /** - * This constructor can only be used for non parameterized types. For parameterized ones, please - * use {@link #TypeCodec(DataType, TypeToken)} instead. - * - * @param javaClass The Java class this codec serializes from and deserializes to. - */ - protected TypeCodec(DataType cqlType, Class javaClass) { - this(cqlType, TypeToken.of(javaClass)); - } - - protected TypeCodec(DataType cqlType, TypeToken javaType) { - checkNotNull(cqlType, "cqlType cannot be null"); - checkNotNull(javaType, "javaType cannot be null"); - checkArgument( - !javaType.isPrimitive(), - "Cannot create a codec for a primitive Java type (%s), please use the wrapper type instead", - javaType); - this.cqlType = cqlType; - this.javaType = javaType; - } - - /** - * Return the Java type that this codec deserializes to and serializes from. - * - * @return The Java type this codec deserializes to and serializes from. - */ - public TypeToken getJavaType() { - return javaType; - } - - /** - * Return the CQL type that this codec deserializes from and serializes to. - * - * @return The Java type this codec deserializes from and serializes to. - */ - public DataType getCqlType() { - return cqlType; - } - - /** - * Serialize the given value according to the CQL type handled by this codec. - * - *

Implementation notes: - * - *

    - *
  1. Null values should be gracefully handled and no exception should be raised; these should - * be considered as the equivalent of a NULL CQL value; - *
  2. Codecs for CQL collection types should not permit null elements; - *
  3. Codecs for CQL collection types should treat a {@code null} input as the equivalent of an - * empty collection. - *
- * - * @param value An instance of T; may be {@code null}. - * @param protocolVersion the protocol version to use when serializing {@code bytes}. In most - * cases, the proper value to provide for this argument is the value returned by {@link - * ProtocolOptions#getProtocolVersion} (which is the protocol version in use by the driver). - * @return A {@link ByteBuffer} instance containing the serialized form of T - * @throws InvalidTypeException if the given value does not have the expected type - */ - public abstract ByteBuffer serialize(T value, ProtocolVersion protocolVersion) - throws InvalidTypeException; - - /** - * Deserialize the given {@link ByteBuffer} instance according to the CQL type handled by this - * codec. - * - *

Implementation notes: - * - *

    - *
  1. Null or empty buffers should be gracefully handled and no exception should be raised; - * these should be considered as the equivalent of a NULL CQL value and, in most cases, - * should map to {@code null} or a default value for the corresponding Java type, if - * applicable; - *
  2. Codecs for CQL collection types should clearly document whether they return immutable - * collections or not (note that the driver's default collection codecs return - * mutable collections); - *
  3. Codecs for CQL collection types should avoid returning {@code null}; they should return - * empty collections instead (the driver's default collection codecs all comply with this - * rule). - *
  4. The provided {@link ByteBuffer} should never be consumed by read operations that modify - * its current position; if necessary, {@link ByteBuffer#duplicate()} duplicate} it before - * consuming. - *
- * - * @param bytes A {@link ByteBuffer} instance containing the serialized form of T; may be {@code - * null} or empty. - * @param protocolVersion the protocol version to use when serializing {@code bytes}. In most - * cases, the proper value to provide for this argument is the value returned by {@link - * ProtocolOptions#getProtocolVersion} (which is the protocol version in use by the driver). - * @return An instance of T - * @throws InvalidTypeException if the given {@link ByteBuffer} instance cannot be deserialized - */ - public abstract T deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) - throws InvalidTypeException; - - /** - * Parse the given CQL literal into an instance of the Java type handled by this codec. - * - *

Implementors should take care of unquoting and unescaping the given CQL string where - * applicable. Null values and empty Strings should be accepted, as well as the string {@code - * "NULL"}; in most cases, implementations should interpret these inputs has equivalent to a - * {@code null} reference. - * - *

Implementing this method is not strictly mandatory: internally, the driver only uses it to - * parse the INITCOND when building the metadata of an aggregate function (and in most cases it - * will use a built-in codec, unless the INITCOND has a custom type). - * - * @param value The CQL string to parse, may be {@code null} or empty. - * @return An instance of T; may be {@code null} on a {@code null input}. - * @throws InvalidTypeException if the given value cannot be parsed into the expected type - */ - public abstract T parse(String value) throws InvalidTypeException; - - /** - * Format the given value as a valid CQL literal according to the CQL type handled by this codec. - * - *

Implementors should take care of quoting and escaping the resulting CQL literal where - * applicable. Null values should be accepted; in most cases, implementations should return the - * CQL keyword {@code "NULL"} for {@code null} inputs. - * - *

Implementing this method is not strictly mandatory. It is used: - * - *

    - *
  1. in the query builder, when values are inlined in the query string (see {@link - * com.datastax.driver.core.querybuilder.BuiltStatement} for a detailed explanation of when - * this happens); - *
  2. in the {@link QueryLogger}, if parameter logging is enabled; - *
  3. to format the INITCOND in {@link AggregateMetadata#asCQLQuery(boolean)}; - *
  4. in the {@code toString()} implementation of some objects ({@link UDTValue}, {@link - * TupleValue}, and the internal representation of a {@code ROWS} response), which may - * appear in driver logs. - *
- * - * If you choose not to implement this method, you should not throw an exception but instead - * return a constant string (for example "XxxCodec.format not implemented"). - * - * @param value An instance of T; may be {@code null}. - * @return CQL string - * @throws InvalidTypeException if the given value does not have the expected type - */ - public abstract String format(T value) throws InvalidTypeException; - - /** - * Return {@code true} if this codec is capable of serializing the given {@code javaType}. - * - *

The implementation is invariant with respect to the passed argument (through the - * usage of {@link TypeToken#equals(Object)} and it's strongly recommended not to modify this - * behavior. This means that a codec will only ever return {@code true} for the - * exact Java type that it has been created for. - * - *

If the argument represents a Java primitive type, its wrapper type is considered instead. - * - * @param javaType The Java type this codec should serialize from and deserialize to; cannot be - * {@code null}. - * @return {@code true} if the codec is capable of serializing the given {@code javaType}, and - * {@code false} otherwise. - * @throws NullPointerException if {@code javaType} is {@code null}. - */ - public boolean accepts(TypeToken javaType) { - checkNotNull(javaType, "Parameter javaType cannot be null"); - return this.javaType.equals(javaType.wrap()); - } - - /** - * Return {@code true} if this codec is capable of serializing the given {@code javaType}. - * - *

This implementation simply compares the given type against this codec's runtime (raw) type - * for equality; it is invariant with respect to the passed argument (through the usage - * of {@link Class#equals(Object)} and it's strongly recommended not to modify this - * behavior. This means that a codec will only ever return {@code true} for the - * exact runtime (raw) Java type that it has been created for. - * - * @param javaType The Java type this codec should serialize from and deserialize to; cannot be - * {@code null}. - * @return {@code true} if the codec is capable of serializing the given {@code javaType}, and - * {@code false} otherwise. - * @throws NullPointerException if {@code javaType} is {@code null}. - */ - public boolean accepts(Class javaType) { - checkNotNull(javaType, "Parameter javaType cannot be null"); - if (javaType.isPrimitive()) { - if (javaType == Boolean.TYPE) { - javaType = Boolean.class; - } else if (javaType == Character.TYPE) { - javaType = Character.class; - } else if (javaType == Byte.TYPE) { - javaType = Byte.class; - } else if (javaType == Short.TYPE) { - javaType = Short.class; - } else if (javaType == Integer.TYPE) { - javaType = Integer.class; - } else if (javaType == Long.TYPE) { - javaType = Long.class; - } else if (javaType == Float.TYPE) { - javaType = Float.class; - } else if (javaType == Double.TYPE) { - javaType = Double.class; - } - } - return this.javaType.getRawType().equals(javaType); - } - - /** - * Return {@code true} if this codec is capable of deserializing the given {@code cqlType}. - * - * @param cqlType The CQL type this codec should deserialize from and serialize to; cannot be - * {@code null}. - * @return {@code true} if the codec is capable of deserializing the given {@code cqlType}, and - * {@code false} otherwise. - * @throws NullPointerException if {@code cqlType} is {@code null}. - */ - public boolean accepts(DataType cqlType) { - checkNotNull(cqlType, "Parameter cqlType cannot be null"); - return this.cqlType.equals(cqlType); - } - - /** - * Return {@code true} if this codec is capable of serializing the given object. Note that the - * object's Java type is inferred from the object's runtime (raw) type, contrary to {@link - * #accepts(TypeToken)} which is capable of handling generic types. - * - *

This method is intended mostly to be used by the QueryBuilder when no type information is - * available when the codec is used. - * - *

Implementation notes: - * - *

    - *
  1. The default implementation is covariant with respect to the passed argument - * (through the usage of {@link Class#isAssignableFrom(Class)}) and it's strongly - * recommended not to modify this behavior. This means that, by default, a codec will - * accept any subtype of the Java type that it has been created for. - *
  2. The base implementation provided here can only handle non-parameterized types; codecs - * handling parameterized types, such as collection types, must override this method and - * perform some sort of "manual" inspection of the actual type parameters. - *
  3. Similarly, codecs that only accept a partial subset of all possible values must override - * this method and manually inspect the object to check if it complies or not with the - * codec's limitations. - *
- * - * @param value The Java type this codec should serialize from and deserialize to; cannot be - * {@code null}. - * @return {@code true} if the codec is capable of serializing the given {@code javaType}, and - * {@code false} otherwise. - * @throws NullPointerException if {@code value} is {@code null}. - */ - public boolean accepts(Object value) { - checkNotNull(value, "Parameter value cannot be null"); - return javaType.getRawType().isAssignableFrom(value.getClass()); - } - - @Override - public String toString() { - return String.format("%s [%s <-> %s]", this.getClass().getSimpleName(), cqlType, javaType); - } - - /** - * A codec that is capable of handling primitive booleans, thus avoiding the overhead of boxing - * and unboxing such primitives. - */ - public abstract static class PrimitiveBooleanCodec extends TypeCodec { - - protected PrimitiveBooleanCodec(DataType cqlType) { - super(cqlType, Boolean.class); - } - - public abstract ByteBuffer serializeNoBoxing(boolean v, ProtocolVersion protocolVersion); - - public abstract boolean deserializeNoBoxing(ByteBuffer v, ProtocolVersion protocolVersion); - - @Override - public ByteBuffer serialize(Boolean value, ProtocolVersion protocolVersion) { - return value == null ? null : serializeNoBoxing(value, protocolVersion); - } - - @Override - public Boolean deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 - ? null - : deserializeNoBoxing(bytes, protocolVersion); - } - } - - /** - * A codec that is capable of handling primitive bytes, thus avoiding the overhead of boxing and - * unboxing such primitives. - */ - public abstract static class PrimitiveByteCodec extends TypeCodec { - - protected PrimitiveByteCodec(DataType cqlType) { - super(cqlType, Byte.class); - } - - public abstract ByteBuffer serializeNoBoxing(byte v, ProtocolVersion protocolVersion); - - public abstract byte deserializeNoBoxing(ByteBuffer v, ProtocolVersion protocolVersion); - - @Override - public ByteBuffer serialize(Byte value, ProtocolVersion protocolVersion) { - return value == null ? null : serializeNoBoxing(value, protocolVersion); - } - - @Override - public Byte deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 - ? null - : deserializeNoBoxing(bytes, protocolVersion); - } - } - - /** - * A codec that is capable of handling primitive shorts, thus avoiding the overhead of boxing and - * unboxing such primitives. - */ - public abstract static class PrimitiveShortCodec extends TypeCodec { - - protected PrimitiveShortCodec(DataType cqlType) { - super(cqlType, Short.class); - } - - public abstract ByteBuffer serializeNoBoxing(short v, ProtocolVersion protocolVersion); - - public abstract short deserializeNoBoxing(ByteBuffer v, ProtocolVersion protocolVersion); - - @Override - public ByteBuffer serialize(Short value, ProtocolVersion protocolVersion) { - return value == null ? null : serializeNoBoxing(value, protocolVersion); - } - - @Override - public Short deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 - ? null - : deserializeNoBoxing(bytes, protocolVersion); - } - } - - /** - * A codec that is capable of handling primitive ints, thus avoiding the overhead of boxing and - * unboxing such primitives. - */ - public abstract static class PrimitiveIntCodec extends TypeCodec { - - protected PrimitiveIntCodec(DataType cqlType) { - super(cqlType, Integer.class); - } - - public abstract ByteBuffer serializeNoBoxing(int v, ProtocolVersion protocolVersion); - - public abstract int deserializeNoBoxing(ByteBuffer v, ProtocolVersion protocolVersion); - - @Override - public ByteBuffer serialize(Integer value, ProtocolVersion protocolVersion) { - return value == null ? null : serializeNoBoxing(value, protocolVersion); - } - - @Override - public Integer deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 - ? null - : deserializeNoBoxing(bytes, protocolVersion); - } - } - - /** - * A codec that is capable of handling primitive longs, thus avoiding the overhead of boxing and - * unboxing such primitives. - */ - public abstract static class PrimitiveLongCodec extends TypeCodec { - - protected PrimitiveLongCodec(DataType cqlType) { - super(cqlType, Long.class); - } - - public abstract ByteBuffer serializeNoBoxing(long v, ProtocolVersion protocolVersion); - - public abstract long deserializeNoBoxing(ByteBuffer v, ProtocolVersion protocolVersion); - - @Override - public ByteBuffer serialize(Long value, ProtocolVersion protocolVersion) { - return value == null ? null : serializeNoBoxing(value, protocolVersion); - } - - @Override - public Long deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 - ? null - : deserializeNoBoxing(bytes, protocolVersion); - } - } - - /** - * A codec that is capable of handling primitive floats, thus avoiding the overhead of boxing and - * unboxing such primitives. - */ - public abstract static class PrimitiveFloatCodec extends TypeCodec { - - protected PrimitiveFloatCodec(DataType cqlType) { - super(cqlType, Float.class); - } - - public abstract ByteBuffer serializeNoBoxing(float v, ProtocolVersion protocolVersion); - - public abstract float deserializeNoBoxing(ByteBuffer v, ProtocolVersion protocolVersion); - - @Override - public ByteBuffer serialize(Float value, ProtocolVersion protocolVersion) { - return value == null ? null : serializeNoBoxing(value, protocolVersion); - } - - @Override - public Float deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 - ? null - : deserializeNoBoxing(bytes, protocolVersion); - } - } - - /** - * A codec that is capable of handling primitive doubles, thus avoiding the overhead of boxing and - * unboxing such primitives. - */ - public abstract static class PrimitiveDoubleCodec extends TypeCodec { - - protected PrimitiveDoubleCodec(DataType cqlType) { - super(cqlType, Double.class); - } - - public abstract ByteBuffer serializeNoBoxing(double v, ProtocolVersion protocolVersion); - - public abstract double deserializeNoBoxing(ByteBuffer v, ProtocolVersion protocolVersion); - - @Override - public ByteBuffer serialize(Double value, ProtocolVersion protocolVersion) { - return value == null ? null : serializeNoBoxing(value, protocolVersion); - } - - @Override - public Double deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 - ? null - : deserializeNoBoxing(bytes, protocolVersion); - } - } - - /** - * Base class for codecs handling CQL string types such as {@link DataType#varchar()}, {@link - * DataType#text()} or {@link DataType#ascii()}. - */ - private abstract static class StringCodec extends TypeCodec { - - private final Charset charset; - - private StringCodec(DataType cqlType, Charset charset) { - super(cqlType, String.class); - this.charset = charset; - } - - @Override - public String parse(String value) { - if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; - if (!ParseUtils.isQuoted(value)) - throw new InvalidTypeException("text or varchar values must be enclosed by single quotes"); - - return ParseUtils.unquote(value); - } - - @Override - public String format(String value) { - if (value == null) return "NULL"; - return ParseUtils.quote(value); - } - - @Override - public ByteBuffer serialize(String value, ProtocolVersion protocolVersion) { - return value == null ? null : ByteBuffer.wrap(value.getBytes(charset)); - } - - /** - * {@inheritDoc} - * - *

Implementation note: this method treats {@code null}s and empty buffers differently: the - * formers are mapped to {@code null}s while the latters are mapped to empty strings. - */ - @Override - public String deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null) return null; - if (bytes.remaining() == 0) return ""; - return new String(Bytes.getArray(bytes), charset); - } - } - - /** - * This codec maps a CQL {@link DataType#varchar()} to a Java {@link String}. Note that this codec - * also handles {@link DataType#text()}, which is merely an alias for {@link DataType#varchar()}. - */ - private static class VarcharCodec extends StringCodec { - - private static final VarcharCodec instance = new VarcharCodec(); - - private VarcharCodec() { - super(DataType.varchar(), Charset.forName("UTF-8")); - } - } - - /** This codec maps a CQL {@link DataType#ascii()} to a Java {@link String}. */ - private static class AsciiCodec extends StringCodec { - - private static final AsciiCodec instance = new AsciiCodec(); - - private static final Pattern ASCII_PATTERN = Pattern.compile("^\\p{ASCII}*$"); - - private AsciiCodec() { - super(DataType.ascii(), Charset.forName("US-ASCII")); - } - - @Override - public ByteBuffer serialize(String value, ProtocolVersion protocolVersion) { - if (value != null && !ASCII_PATTERN.matcher(value).matches()) { - throw new InvalidTypeException(String.format("%s is not a valid ASCII String", value)); - } - return super.serialize(value, protocolVersion); - } - - @Override - public String format(String value) { - if (value != null && !ASCII_PATTERN.matcher(value).matches()) { - throw new InvalidTypeException(String.format("%s is not a valid ASCII String", value)); - } - return super.format(value); - } - } - - /** - * Base class for codecs handling CQL 8-byte integer types such as {@link DataType#bigint()}, - * {@link DataType#counter()} or {@link DataType#time()}. - */ - private abstract static class LongCodec extends PrimitiveLongCodec { - - private LongCodec(DataType cqlType) { - super(cqlType); - } - - @Override - public Long parse(String value) { - try { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : Long.parseLong(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse 64-bits long value from \"%s\"", value)); - } - } - - @Override - public String format(Long value) { - if (value == null) return "NULL"; - return Long.toString(value); - } - - @Override - public ByteBuffer serializeNoBoxing(long value, ProtocolVersion protocolVersion) { - ByteBuffer bb = ByteBuffer.allocate(8); - bb.putLong(0, value); - return bb; - } - - @Override - public long deserializeNoBoxing(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return 0; - if (bytes.remaining() != 8) - throw new InvalidTypeException( - "Invalid 64-bits long value, expecting 8 bytes but got " + bytes.remaining()); - - return bytes.getLong(bytes.position()); - } - } - - /** This codec maps a CQL {@link DataType#bigint()} to a Java {@link Long}. */ - private static class BigintCodec extends LongCodec { - - private static final BigintCodec instance = new BigintCodec(); - - private BigintCodec() { - super(DataType.bigint()); - } - } - - /** This codec maps a CQL {@link DataType#counter()} to a Java {@link Long}. */ - private static class CounterCodec extends LongCodec { - - private static final CounterCodec instance = new CounterCodec(); - - private CounterCodec() { - super(DataType.counter()); - } - } - - /** This codec maps a CQL {@link DataType#blob()} to a Java {@link ByteBuffer}. */ - private static class BlobCodec extends TypeCodec { - - private static final BlobCodec instance = new BlobCodec(); - - private BlobCodec() { - super(DataType.blob(), ByteBuffer.class); - } - - @Override - public ByteBuffer parse(String value) { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : Bytes.fromHexString(value); - } - - @Override - public String format(ByteBuffer value) { - if (value == null) return "NULL"; - return Bytes.toHexString(value); - } - - @Override - public ByteBuffer serialize(ByteBuffer value, ProtocolVersion protocolVersion) { - return value == null ? null : value.duplicate(); - } - - @Override - public ByteBuffer deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null ? null : bytes.duplicate(); - } - } - - /** - * This codec maps a CQL {@link DataType#custom(String) custom} type to a Java {@link ByteBuffer}. - * Note that no instance of this codec is part of the default set of codecs used by the Java - * driver; instances of this codec must be manually registered. - */ - private static class CustomCodec extends TypeCodec { - - private CustomCodec(DataType custom) { - super(custom, ByteBuffer.class); - assert custom.getName() == Name.CUSTOM; - } - - @Override - public ByteBuffer parse(String value) { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : Bytes.fromHexString(value); - } - - @Override - public String format(ByteBuffer value) { - if (value == null) return "NULL"; - return Bytes.toHexString(value); - } - - @Override - public ByteBuffer serialize(ByteBuffer value, ProtocolVersion protocolVersion) { - return value == null ? null : value.duplicate(); - } - - @Override - public ByteBuffer deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null ? null : bytes.duplicate(); - } - } - - /** This codec maps a CQL {@link DataType#cboolean()} to a Java {@link Boolean}. */ - private static class BooleanCodec extends PrimitiveBooleanCodec { - - private static final ByteBuffer TRUE = ByteBuffer.wrap(new byte[] {1}); - private static final ByteBuffer FALSE = ByteBuffer.wrap(new byte[] {0}); - - private static final BooleanCodec instance = new BooleanCodec(); - - private BooleanCodec() { - super(DataType.cboolean()); - } - - @Override - public Boolean parse(String value) { - if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; - if (value.equalsIgnoreCase(Boolean.FALSE.toString())) return false; - if (value.equalsIgnoreCase(Boolean.TRUE.toString())) return true; - - throw new InvalidTypeException( - String.format("Cannot parse boolean value from \"%s\"", value)); - } - - @Override - public String format(Boolean value) { - if (value == null) return "NULL"; - return value ? "true" : "false"; - } - - @Override - public ByteBuffer serializeNoBoxing(boolean value, ProtocolVersion protocolVersion) { - return value ? TRUE.duplicate() : FALSE.duplicate(); - } - - @Override - public boolean deserializeNoBoxing(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return false; - if (bytes.remaining() != 1) - throw new InvalidTypeException( - "Invalid boolean value, expecting 1 byte but got " + bytes.remaining()); - - return bytes.get(bytes.position()) != 0; - } - } - - /** This codec maps a CQL {@link DataType#decimal()} to a Java {@link BigDecimal}. */ - private static class DecimalCodec extends TypeCodec { - - private static final DecimalCodec instance = new DecimalCodec(); - - private DecimalCodec() { - super(DataType.decimal(), BigDecimal.class); - } - - @Override - public BigDecimal parse(String value) { - try { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : new BigDecimal(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse decimal value from \"%s\"", value)); - } - } - - @Override - public String format(BigDecimal value) { - if (value == null) return "NULL"; - return value.toString(); - } - - @Override - public ByteBuffer serialize(BigDecimal value, ProtocolVersion protocolVersion) { - if (value == null) return null; - BigInteger bi = value.unscaledValue(); - int scale = value.scale(); - byte[] bibytes = bi.toByteArray(); - - ByteBuffer bytes = ByteBuffer.allocate(4 + bibytes.length); - bytes.putInt(scale); - bytes.put(bibytes); - bytes.rewind(); - return bytes; - } - - @Override - public BigDecimal deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return null; - if (bytes.remaining() < 4) - throw new InvalidTypeException( - "Invalid decimal value, expecting at least 4 bytes but got " + bytes.remaining()); - - bytes = bytes.duplicate(); - int scale = bytes.getInt(); - byte[] bibytes = new byte[bytes.remaining()]; - bytes.get(bibytes); - - BigInteger bi = new BigInteger(bibytes); - return new BigDecimal(bi, scale); - } - } - - /** This codec maps a CQL {@link DataType#cdouble()} to a Java {@link Double}. */ - private static class DoubleCodec extends PrimitiveDoubleCodec { - - private static final DoubleCodec instance = new DoubleCodec(); - - private DoubleCodec() { - super(DataType.cdouble()); - } - - @Override - public Double parse(String value) { - try { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : Double.parseDouble(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse 64-bits double value from \"%s\"", value)); - } - } - - @Override - public String format(Double value) { - if (value == null) return "NULL"; - return Double.toString(value); - } - - @Override - public ByteBuffer serializeNoBoxing(double value, ProtocolVersion protocolVersion) { - ByteBuffer bb = ByteBuffer.allocate(8); - bb.putDouble(0, value); - return bb; - } - - @Override - public double deserializeNoBoxing(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return 0; - if (bytes.remaining() != 8) - throw new InvalidTypeException( - "Invalid 64-bits double value, expecting 8 bytes but got " + bytes.remaining()); - - return bytes.getDouble(bytes.position()); - } - } - - /** This codec maps a CQL {@link DataType#cfloat()} to a Java {@link Float}. */ - private static class FloatCodec extends PrimitiveFloatCodec { - - private static final FloatCodec instance = new FloatCodec(); - - private FloatCodec() { - super(DataType.cfloat()); - } - - @Override - public Float parse(String value) { - try { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : Float.parseFloat(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse 32-bits float value from \"%s\"", value)); - } - } - - @Override - public String format(Float value) { - if (value == null) return "NULL"; - return Float.toString(value); - } - - @Override - public ByteBuffer serializeNoBoxing(float value, ProtocolVersion protocolVersion) { - ByteBuffer bb = ByteBuffer.allocate(4); - bb.putFloat(0, value); - return bb; - } - - @Override - public float deserializeNoBoxing(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return 0; - if (bytes.remaining() != 4) - throw new InvalidTypeException( - "Invalid 32-bits float value, expecting 4 bytes but got " + bytes.remaining()); - - return bytes.getFloat(bytes.position()); - } - } - - /** This codec maps a CQL {@link DataType#inet()} to a Java {@link InetAddress}. */ - private static class InetCodec extends TypeCodec { - - private static final InetCodec instance = new InetCodec(); - - private InetCodec() { - super(DataType.inet(), InetAddress.class); - } - - @Override - public InetAddress parse(String value) { - if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; - - value = value.trim(); - if (!ParseUtils.isQuoted(value)) - throw new InvalidTypeException( - String.format("inet values must be enclosed in single quotes (\"%s\")", value)); - try { - return InetAddress.getByName(value.substring(1, value.length() - 1)); - } catch (Exception e) { - throw new InvalidTypeException(String.format("Cannot parse inet value from \"%s\"", value)); - } - } - - @Override - public String format(InetAddress value) { - if (value == null) return "NULL"; - return "'" + value.getHostAddress() + "'"; - } - - @Override - public ByteBuffer serialize(InetAddress value, ProtocolVersion protocolVersion) { - return value == null ? null : ByteBuffer.wrap(value.getAddress()); - } - - @Override - public InetAddress deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return null; - try { - return InetAddress.getByAddress(Bytes.getArray(bytes)); - } catch (UnknownHostException e) { - throw new InvalidTypeException( - "Invalid bytes for inet value, got " + bytes.remaining() + " bytes"); - } - } - } - - /** This codec maps a CQL {@link DataType#tinyint()} to a Java {@link Byte}. */ - private static class TinyIntCodec extends PrimitiveByteCodec { - - private static final TinyIntCodec instance = new TinyIntCodec(); - - private TinyIntCodec() { - super(tinyint()); - } - - @Override - public Byte parse(String value) { - try { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : Byte.parseByte(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse 8-bits int value from \"%s\"", value)); - } - } - - @Override - public String format(Byte value) { - if (value == null) return "NULL"; - return Byte.toString(value); - } - - @Override - public ByteBuffer serializeNoBoxing(byte value, ProtocolVersion protocolVersion) { - ByteBuffer bb = ByteBuffer.allocate(1); - bb.put(0, value); - return bb; - } - - @Override - public byte deserializeNoBoxing(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return 0; - if (bytes.remaining() != 1) - throw new InvalidTypeException( - "Invalid 8-bits integer value, expecting 1 byte but got " + bytes.remaining()); - - return bytes.get(bytes.position()); - } - } - - /** This codec maps a CQL {@link DataType#smallint()} to a Java {@link Short}. */ - private static class SmallIntCodec extends PrimitiveShortCodec { - - private static final SmallIntCodec instance = new SmallIntCodec(); - - private SmallIntCodec() { - super(smallint()); - } - - @Override - public Short parse(String value) { - try { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : Short.parseShort(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse 16-bits int value from \"%s\"", value)); - } - } - - @Override - public String format(Short value) { - if (value == null) return "NULL"; - return Short.toString(value); - } - - @Override - public ByteBuffer serializeNoBoxing(short value, ProtocolVersion protocolVersion) { - ByteBuffer bb = ByteBuffer.allocate(2); - bb.putShort(0, value); - return bb; - } - - @Override - public short deserializeNoBoxing(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return 0; - if (bytes.remaining() != 2) - throw new InvalidTypeException( - "Invalid 16-bits integer value, expecting 2 bytes but got " + bytes.remaining()); - - return bytes.getShort(bytes.position()); - } - } - - /** This codec maps a CQL {@link DataType#cint()} to a Java {@link Integer}. */ - private static class IntCodec extends PrimitiveIntCodec { - - private static final IntCodec instance = new IntCodec(); - - private IntCodec() { - super(DataType.cint()); - } - - @Override - public Integer parse(String value) { - try { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : Integer.parseInt(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse 32-bits int value from \"%s\"", value)); - } - } - - @Override - public String format(Integer value) { - if (value == null) return "NULL"; - return Integer.toString(value); - } - - @Override - public ByteBuffer serializeNoBoxing(int value, ProtocolVersion protocolVersion) { - ByteBuffer bb = ByteBuffer.allocate(4); - bb.putInt(0, value); - return bb; - } - - @Override - public int deserializeNoBoxing(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return 0; - if (bytes.remaining() != 4) - throw new InvalidTypeException( - "Invalid 32-bits integer value, expecting 4 bytes but got " + bytes.remaining()); - - return bytes.getInt(bytes.position()); - } - } - - /** This codec maps a CQL {@link DataType#timestamp()} to a Java {@link Date}. */ - private static class TimestampCodec extends TypeCodec { - - private static final TimestampCodec instance = new TimestampCodec(); - - private TimestampCodec() { - super(DataType.timestamp(), Date.class); - } - - @Override - public Date parse(String value) { - if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; - // strip enclosing single quotes, if any - if (ParseUtils.isQuoted(value)) value = ParseUtils.unquote(value); - - if (ParseUtils.isLongLiteral(value)) { - try { - return new Date(Long.parseLong(value)); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse timestamp value from \"%s\"", value)); - } - } - - try { - return ParseUtils.parseDate(value); - } catch (ParseException e) { - throw new InvalidTypeException( - String.format("Cannot parse timestamp value from \"%s\"", value)); - } - } - - @Override - public String format(Date value) { - if (value == null) return "NULL"; - return Long.toString(value.getTime()); - } - - @Override - public ByteBuffer serialize(Date value, ProtocolVersion protocolVersion) { - return value == null - ? null - : BigintCodec.instance.serializeNoBoxing(value.getTime(), protocolVersion); - } - - @Override - public Date deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 - ? null - : new Date(BigintCodec.instance.deserializeNoBoxing(bytes, protocolVersion)); - } - } - - /** This codec maps a CQL {@link DataType#date()} to the custom {@link LocalDate} class. */ - private static class DateCodec extends TypeCodec { - - private static final DateCodec instance = new DateCodec(); - - private static final String pattern = "yyyy-MM-dd"; - - private DateCodec() { - super(DataType.date(), LocalDate.class); - } - - @Override - public LocalDate parse(String value) { - if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; - - // single quotes are optional for long literals, mandatory for date patterns - // strip enclosing single quotes, if any - if (ParseUtils.isQuoted(value)) value = ParseUtils.unquote(value); - - if (ParseUtils.isLongLiteral(value)) { - long unsigned; - try { - unsigned = Long.parseLong(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse date value from \"%s\"", value), e); - } - try { - int days = CodecUtils.fromCqlDateToDaysSinceEpoch(unsigned); - return LocalDate.fromDaysSinceEpoch(days); - } catch (IllegalArgumentException e) { - throw new InvalidTypeException( - String.format("Cannot parse date value from \"%s\"", value), e); - } - } - - try { - Date date = ParseUtils.parseDate(value, pattern); - return LocalDate.fromMillisSinceEpoch(date.getTime()); - } catch (ParseException e) { - throw new InvalidTypeException( - String.format("Cannot parse date value from \"%s\"", value), e); - } - } - - @Override - public String format(LocalDate value) { - if (value == null) return "NULL"; - return ParseUtils.quote(value.toString()); - } - - @Override - public ByteBuffer serialize(LocalDate value, ProtocolVersion protocolVersion) { - if (value == null) return null; - int unsigned = CodecUtils.fromSignedToUnsignedInt(value.getDaysSinceEpoch()); - return IntCodec.instance.serializeNoBoxing(unsigned, protocolVersion); - } - - @Override - public LocalDate deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return null; - int unsigned = IntCodec.instance.deserializeNoBoxing(bytes, protocolVersion); - int signed = CodecUtils.fromUnsignedToSignedInt(unsigned); - return LocalDate.fromDaysSinceEpoch(signed); - } - } - - /** This codec maps a CQL {@link DataType#time()} to a Java {@link Long}. */ - private static class TimeCodec extends LongCodec { - - private static final TimeCodec instance = new TimeCodec(); - - private TimeCodec() { - super(DataType.time()); - } - - @Override - public Long parse(String value) { - if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; - - // enclosing single quotes required, even for long literals - if (!ParseUtils.isQuoted(value)) - throw new InvalidTypeException("time values must be enclosed by single quotes"); - value = value.substring(1, value.length() - 1); - - if (ParseUtils.isLongLiteral(value)) { - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse time value from \"%s\"", value), e); - } - } - - try { - return ParseUtils.parseTime(value); - } catch (ParseException e) { - throw new InvalidTypeException( - String.format("Cannot parse time value from \"%s\"", value), e); - } - } - - @Override - public String format(Long value) { - if (value == null) return "NULL"; - return ParseUtils.quote(ParseUtils.formatTime(value)); - } - } - - /** - * Base class for codecs handling CQL UUID types such as {@link DataType#uuid()} and {@link - * DataType#timeuuid()}. - */ - private abstract static class AbstractUUIDCodec extends TypeCodec { - - private AbstractUUIDCodec(DataType cqlType) { - super(cqlType, UUID.class); - } - - @Override - public UUID parse(String value) { - try { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : UUID.fromString(value); - } catch (IllegalArgumentException e) { - throw new InvalidTypeException( - String.format("Cannot parse UUID value from \"%s\"", value), e); - } - } - - @Override - public String format(UUID value) { - if (value == null) return "NULL"; - return value.toString(); - } - - @Override - public ByteBuffer serialize(UUID value, ProtocolVersion protocolVersion) { - if (value == null) return null; - ByteBuffer bb = ByteBuffer.allocate(16); - bb.putLong(0, value.getMostSignificantBits()); - bb.putLong(8, value.getLeastSignificantBits()); - return bb; - } - - @Override - public UUID deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 - ? null - : new UUID(bytes.getLong(bytes.position()), bytes.getLong(bytes.position() + 8)); - } - } - - /** This codec maps a CQL {@link DataType#uuid()} to a Java {@link UUID}. */ - private static class UUIDCodec extends AbstractUUIDCodec { - - private static final UUIDCodec instance = new UUIDCodec(); - - private UUIDCodec() { - super(DataType.uuid()); - } - } - - /** This codec maps a CQL {@link DataType#timeuuid()} to a Java {@link UUID}. */ - private static class TimeUUIDCodec extends AbstractUUIDCodec { - - private static final TimeUUIDCodec instance = new TimeUUIDCodec(); - - private TimeUUIDCodec() { - super(timeuuid()); - } - - @Override - public String format(UUID value) { - if (value == null) return "NULL"; - if (value.version() != 1) - throw new InvalidTypeException( - String.format("%s is not a Type 1 (time-based) UUID", value)); - return super.format(value); - } - - @Override - public ByteBuffer serialize(UUID value, ProtocolVersion protocolVersion) { - if (value == null) return null; - if (value.version() != 1) - throw new InvalidTypeException( - String.format("%s is not a Type 1 (time-based) UUID", value)); - return super.serialize(value, protocolVersion); - } - } - - /** This codec maps a CQL {@link DataType#varint()} to a Java {@link BigInteger}. */ - private static class VarintCodec extends TypeCodec { - - private static final VarintCodec instance = new VarintCodec(); - - private VarintCodec() { - super(DataType.varint(), BigInteger.class); - } - - @Override - public BigInteger parse(String value) { - try { - return value == null || value.isEmpty() || value.equalsIgnoreCase("NULL") - ? null - : new BigInteger(value); - } catch (NumberFormatException e) { - throw new InvalidTypeException( - String.format("Cannot parse varint value from \"%s\"", value), e); - } - } - - @Override - public String format(BigInteger value) { - if (value == null) return "NULL"; - return value.toString(); - } - - @Override - public ByteBuffer serialize(BigInteger value, ProtocolVersion protocolVersion) { - return value == null ? null : ByteBuffer.wrap(value.toByteArray()); - } - - @Override - public BigInteger deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - return bytes == null || bytes.remaining() == 0 ? null : new BigInteger(Bytes.getArray(bytes)); - } - } - - /** - * Base class for codecs mapping CQL {@link DataType#list(DataType) lists} and {@link - * DataType#set(DataType) sets} to Java collections. - */ - public abstract static class AbstractCollectionCodec> - extends TypeCodec { - - protected final TypeCodec eltCodec; - - protected AbstractCollectionCodec( - CollectionType cqlType, TypeToken javaType, TypeCodec eltCodec) { - super(cqlType, javaType); - checkArgument( - cqlType.getName() == Name.LIST || cqlType.getName() == Name.SET, - "Expecting list or set type, got %s", - cqlType); - this.eltCodec = eltCodec; - } - - @Override - public ByteBuffer serialize(C value, ProtocolVersion protocolVersion) { - if (value == null) return null; - int i = 0; - ByteBuffer[] bbs = new ByteBuffer[value.size()]; - for (E elt : value) { - if (elt == null) { - throw new NullPointerException("Collection elements cannot be null"); - } - ByteBuffer bb; - try { - bb = eltCodec.serialize(elt, protocolVersion); - } catch (ClassCastException e) { - throw new InvalidTypeException( - String.format( - "Invalid type for %s element, expecting %s but got %s", - cqlType, eltCodec.getJavaType(), elt.getClass()), - e); - } - bbs[i++] = bb; - } - return CodecUtils.pack(bbs, value.size(), protocolVersion); - } - - @Override - public C deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return newInstance(0); - try { - ByteBuffer input = bytes.duplicate(); - int size = CodecUtils.readSize(input, protocolVersion); - C coll = newInstance(size); - for (int i = 0; i < size; i++) { - ByteBuffer databb = CodecUtils.readValue(input, protocolVersion); - coll.add(eltCodec.deserialize(databb, protocolVersion)); - } - return coll; - } catch (BufferUnderflowException e) { - throw new InvalidTypeException("Not enough bytes to deserialize collection", e); - } - } - - @Override - public String format(C value) { - if (value == null) return "NULL"; - StringBuilder sb = new StringBuilder(); - sb.append(getOpeningChar()); - int i = 0; - for (E v : value) { - if (i++ != 0) sb.append(","); - sb.append(eltCodec.format(v)); - } - sb.append(getClosingChar()); - return sb.toString(); - } - - @Override - public C parse(String value) { - if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; - - int idx = ParseUtils.skipSpaces(value, 0); - if (value.charAt(idx++) != getOpeningChar()) - throw new InvalidTypeException( - String.format( - "Cannot parse collection value from \"%s\", at character %d expecting '%s' but got '%c'", - value, idx, getOpeningChar(), value.charAt(idx))); - - idx = ParseUtils.skipSpaces(value, idx); - - if (value.charAt(idx) == getClosingChar()) return newInstance(0); - - C l = newInstance(10); - while (idx < value.length()) { - int n; - try { - n = ParseUtils.skipCQLValue(value, idx); - } catch (IllegalArgumentException e) { - throw new InvalidTypeException( - String.format( - "Cannot parse collection value from \"%s\", invalid CQL value at character %d", - value, idx), - e); - } - - l.add(eltCodec.parse(value.substring(idx, n))); - idx = n; - - idx = ParseUtils.skipSpaces(value, idx); - if (value.charAt(idx) == getClosingChar()) return l; - if (value.charAt(idx++) != ',') - throw new InvalidTypeException( - String.format( - "Cannot parse collection value from \"%s\", at character %d expecting ',' but got '%c'", - value, idx, value.charAt(idx))); - - idx = ParseUtils.skipSpaces(value, idx); - } - throw new InvalidTypeException( - String.format( - "Malformed collection value \"%s\", missing closing '%s'", value, getClosingChar())); - } - - @Override - public boolean accepts(Object value) { - checkNotNull(value, "Parameter value cannot be null"); - if (getJavaType().getRawType().isAssignableFrom(value.getClass())) { - // runtime type ok, now check element type - Collection coll = (Collection) value; - if (coll.isEmpty()) return true; - Object elt = coll.iterator().next(); - return eltCodec.accepts(elt); - } - return false; - } - - /** - * Return a new instance of {@code C} with the given estimated size. - * - * @param size The estimated size of the collection to create. - * @return new instance of {@code C} with the given estimated size. - */ - protected abstract C newInstance(int size); - - /** - * Return the opening character to use when formatting values as CQL literals. - * - * @return The opening character to use when formatting values as CQL literals. - */ - private char getOpeningChar() { - return cqlType.getName() == Name.LIST ? '[' : '{'; - } - - /** - * Return the closing character to use when formatting values as CQL literals. - * - * @return The closing character to use when formatting values as CQL literals. - */ - private char getClosingChar() { - return cqlType.getName() == Name.LIST ? ']' : '}'; - } - } - - /** - * This codec maps a CQL {@link DataType#list(DataType) list type} to a Java {@link List}. - * Implementation note: this codec returns mutable, non thread-safe {@link ArrayList} instances. - */ - private static class ListCodec extends AbstractCollectionCodec> { - - private ListCodec(TypeCodec eltCodec) { - super( - DataType.list(eltCodec.getCqlType()), - TypeTokens.listOf(eltCodec.getJavaType()), - eltCodec); - } - - @Override - protected List newInstance(int size) { - return new ArrayList(size); - } - } - - /** - * This codec maps a CQL {@link DataType#set(DataType) set type} to a Java {@link Set}. - * Implementation note: this codec returns mutable, non thread-safe {@link LinkedHashSet} - * instances. - */ - private static class SetCodec extends AbstractCollectionCodec> { - - private SetCodec(TypeCodec eltCodec) { - super(DataType.set(eltCodec.cqlType), TypeTokens.setOf(eltCodec.getJavaType()), eltCodec); - } - - @Override - protected Set newInstance(int size) { - return new LinkedHashSet(size); - } - } - - /** - * Base class for codecs mapping CQL {@link DataType#map(DataType, DataType) maps} to a Java - * {@link Map}. - */ - public abstract static class AbstractMapCodec extends TypeCodec> { - - protected final TypeCodec keyCodec; - - protected final TypeCodec valueCodec; - - protected AbstractMapCodec(TypeCodec keyCodec, TypeCodec valueCodec) { - super( - DataType.map(keyCodec.getCqlType(), valueCodec.getCqlType()), - TypeTokens.mapOf(keyCodec.getJavaType(), valueCodec.getJavaType())); - this.keyCodec = keyCodec; - this.valueCodec = valueCodec; - } - - @Override - public boolean accepts(Object value) { - checkNotNull(value, "Parameter value cannot be null"); - if (value instanceof Map) { - // runtime type ok, now check key and value types - Map map = (Map) value; - if (map.isEmpty()) return true; - Map.Entry entry = map.entrySet().iterator().next(); - return keyCodec.accepts(entry.getKey()) && valueCodec.accepts(entry.getValue()); - } - return false; - } - - @Override - public Map parse(String value) { - if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; - - int idx = ParseUtils.skipSpaces(value, 0); - if (value.charAt(idx++) != '{') - throw new InvalidTypeException( - String.format( - "cannot parse map value from \"%s\", at character %d expecting '{' but got '%c'", - value, idx, value.charAt(idx))); - - idx = ParseUtils.skipSpaces(value, idx); - - if (value.charAt(idx) == '}') return newInstance(0); - - Map m = new HashMap(); - while (idx < value.length()) { - int n; - try { - n = ParseUtils.skipCQLValue(value, idx); - } catch (IllegalArgumentException e) { - throw new InvalidTypeException( - String.format( - "Cannot parse map value from \"%s\", invalid CQL value at character %d", - value, idx), - e); - } - - K k = keyCodec.parse(value.substring(idx, n)); - idx = n; - - idx = ParseUtils.skipSpaces(value, idx); - if (value.charAt(idx++) != ':') - throw new InvalidTypeException( - String.format( - "Cannot parse map value from \"%s\", at character %d expecting ':' but got '%c'", - value, idx, value.charAt(idx))); - idx = ParseUtils.skipSpaces(value, idx); - - try { - n = ParseUtils.skipCQLValue(value, idx); - } catch (IllegalArgumentException e) { - throw new InvalidTypeException( - String.format( - "Cannot parse map value from \"%s\", invalid CQL value at character %d", - value, idx), - e); - } - - V v = valueCodec.parse(value.substring(idx, n)); - idx = n; - - m.put(k, v); - - idx = ParseUtils.skipSpaces(value, idx); - if (value.charAt(idx) == '}') return m; - if (value.charAt(idx++) != ',') - throw new InvalidTypeException( - String.format( - "Cannot parse map value from \"%s\", at character %d expecting ',' but got '%c'", - value, idx, value.charAt(idx))); - - idx = ParseUtils.skipSpaces(value, idx); - } - throw new InvalidTypeException( - String.format("Malformed map value \"%s\", missing closing '}'", value)); - } - - @Override - public String format(Map value) { - if (value == null) return "NULL"; - StringBuilder sb = new StringBuilder(); - sb.append("{"); - int i = 0; - for (Map.Entry e : value.entrySet()) { - if (i++ != 0) sb.append(","); - sb.append(keyCodec.format(e.getKey())); - sb.append(":"); - sb.append(valueCodec.format(e.getValue())); - } - sb.append("}"); - return sb.toString(); - } - - @Override - public ByteBuffer serialize(Map value, ProtocolVersion protocolVersion) { - if (value == null) return null; - int i = 0; - ByteBuffer[] bbs = new ByteBuffer[2 * value.size()]; - for (Map.Entry entry : value.entrySet()) { - ByteBuffer bbk; - K key = entry.getKey(); - if (key == null) { - throw new NullPointerException("Map keys cannot be null"); - } - try { - bbk = keyCodec.serialize(key, protocolVersion); - } catch (ClassCastException e) { - throw new InvalidTypeException( - String.format( - "Invalid type for map key, expecting %s but got %s", - keyCodec.getJavaType(), key.getClass()), - e); - } - ByteBuffer bbv; - V v = entry.getValue(); - if (v == null) { - throw new NullPointerException("Map values cannot be null"); - } - try { - bbv = valueCodec.serialize(v, protocolVersion); - } catch (ClassCastException e) { - throw new InvalidTypeException( - String.format( - "Invalid type for map value, expecting %s but got %s", - valueCodec.getJavaType(), v.getClass()), - e); - } - bbs[i++] = bbk; - bbs[i++] = bbv; - } - return CodecUtils.pack(bbs, value.size(), protocolVersion); - } - - @Override - public Map deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null || bytes.remaining() == 0) return newInstance(0); - try { - ByteBuffer input = bytes.duplicate(); - int n = CodecUtils.readSize(input, protocolVersion); - Map m = newInstance(n); - for (int i = 0; i < n; i++) { - ByteBuffer kbb = CodecUtils.readValue(input, protocolVersion); - ByteBuffer vbb = CodecUtils.readValue(input, protocolVersion); - m.put( - keyCodec.deserialize(kbb, protocolVersion), - valueCodec.deserialize(vbb, protocolVersion)); - } - return m; - } catch (BufferUnderflowException e) { - throw new InvalidTypeException("Not enough bytes to deserialize a map", e); - } - } - - /** - * Return a new {@link Map} instance with the given estimated size. - * - * @param size The estimated size of the collection to create. - * @return A new {@link Map} instance with the given estimated size. - */ - protected abstract Map newInstance(int size); - } - - /** - * This codec maps a CQL {@link DataType#map(DataType, DataType) map type} to a Java {@link Map}. - * Implementation note: this codec returns mutable, non thread-safe {@link LinkedHashMap} - * instances. - */ - private static class MapCodec extends AbstractMapCodec { - - private MapCodec(TypeCodec keyCodec, TypeCodec valueCodec) { - super(keyCodec, valueCodec); - } - - @Override - protected Map newInstance(int size) { - return new LinkedHashMap(size); - } - } - - /** - * Base class for codecs mapping CQL {@link UserType user-defined types} (UDTs) to Java objects. - * It can serve as a base class for codecs dealing with direct UDT-to-Pojo mappings. - * - * @param The Java type that the UDT will be mapped to. - */ - public abstract static class AbstractUDTCodec extends TypeCodec { - - protected final UserType definition; - - protected AbstractUDTCodec(UserType definition, Class javaClass) { - this(definition, TypeToken.of(javaClass)); - } - - protected AbstractUDTCodec(UserType definition, TypeToken javaType) { - super(definition, javaType); - this.definition = definition; - } - - @Override - public ByteBuffer serialize(T value, ProtocolVersion protocolVersion) { - if (value == null) return null; - int size = 0; - int length = definition.size(); - ByteBuffer[] elements = new ByteBuffer[length]; - int i = 0; - for (UserType.Field field : definition) { - elements[i] = - serializeField(value, Metadata.quoteIfNecessary(field.getName()), protocolVersion); - size += 4 + (elements[i] == null ? 0 : elements[i].remaining()); - i++; - } - ByteBuffer result = ByteBuffer.allocate(size); - for (ByteBuffer bb : elements) { - if (bb == null) { - result.putInt(-1); - } else { - result.putInt(bb.remaining()); - result.put(bb.duplicate()); - } - } - return (ByteBuffer) result.flip(); - } - - @Override - public T deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null) return null; - // empty byte buffers will result in empty values - try { - ByteBuffer input = bytes.duplicate(); - T value = newInstance(); - for (UserType.Field field : definition) { - if (!input.hasRemaining()) break; - int n = input.getInt(); - ByteBuffer element = n < 0 ? null : CodecUtils.readBytes(input, n); - value = - deserializeAndSetField( - element, value, Metadata.quoteIfNecessary(field.getName()), protocolVersion); - } - return value; - } catch (BufferUnderflowException e) { - throw new InvalidTypeException("Not enough bytes to deserialize a UDT", e); - } - } - - @Override - public String format(T value) { - if (value == null) return "NULL"; - StringBuilder sb = new StringBuilder("{"); - int i = 0; - for (UserType.Field field : definition) { - if (i > 0) sb.append(","); - sb.append(Metadata.quoteIfNecessary(field.getName())); - sb.append(":"); - sb.append(formatField(value, Metadata.quoteIfNecessary(field.getName()))); - i += 1; - } - sb.append("}"); - return sb.toString(); - } - - @Override - public T parse(String value) { - if (value == null || value.isEmpty() || value.equals("NULL")) return null; - - T v = newInstance(); - - int idx = ParseUtils.skipSpaces(value, 0); - if (value.charAt(idx++) != '{') - throw new InvalidTypeException( - String.format( - "Cannot parse UDT value from \"%s\", at character %d expecting '{' but got '%c'", - value, idx, value.charAt(idx))); - - idx = ParseUtils.skipSpaces(value, idx); - - if (value.charAt(idx) == '}') return v; - - while (idx < value.length()) { - - int n; - try { - n = ParseUtils.skipCQLId(value, idx); - } catch (IllegalArgumentException e) { - throw new InvalidTypeException( - String.format( - "Cannot parse UDT value from \"%s\", cannot parse a CQL identifier at character %d", - value, idx), - e); - } - String name = value.substring(idx, n); - idx = n; - - if (!definition.contains(name)) - throw new InvalidTypeException( - String.format("Unknown field %s in value \"%s\"", name, value)); - - idx = ParseUtils.skipSpaces(value, idx); - if (value.charAt(idx++) != ':') - throw new InvalidTypeException( - String.format( - "Cannot parse UDT value from \"%s\", at character %d expecting ':' but got '%c'", - value, idx, value.charAt(idx))); - idx = ParseUtils.skipSpaces(value, idx); - - try { - n = ParseUtils.skipCQLValue(value, idx); - } catch (IllegalArgumentException e) { - throw new InvalidTypeException( - String.format( - "Cannot parse UDT value from \"%s\", invalid CQL value at character %d", - value, idx), - e); - } - - String input = value.substring(idx, n); - v = parseAndSetField(input, v, name); - idx = n; - - idx = ParseUtils.skipSpaces(value, idx); - if (value.charAt(idx) == '}') return v; - if (value.charAt(idx) != ',') - throw new InvalidTypeException( - String.format( - "Cannot parse UDT value from \"%s\", at character %d expecting ',' but got '%c'", - value, idx, value.charAt(idx))); - ++idx; // skip ',' - - idx = ParseUtils.skipSpaces(value, idx); - } - throw new InvalidTypeException( - String.format("Malformed UDT value \"%s\", missing closing '}'", value)); - } - - /** - * Return a new instance of {@code T}. - * - * @return A new instance of {@code T}. - */ - protected abstract T newInstance(); - - /** - * Serialize an individual field in an object, as part of serializing the whole object to a CQL - * UDT (see {@link #serialize(Object, ProtocolVersion)}). - * - * @param source The object to read the field from. - * @param fieldName The name of the field. Note that if it is case-sensitive or contains special - * characters, it will be double-quoted (i.e. the string will contain actual quote - * characters, as in {@code "\"foobar\""}). - * @param protocolVersion The protocol version to use. - * @return The serialized field, or {@code null} if that field should be ignored. - */ - protected abstract ByteBuffer serializeField( - T source, String fieldName, ProtocolVersion protocolVersion); - - /** - * Deserialize an individual field and set it on an object, as part of deserializing the whole - * object from a CQL UDT (see {@link #deserialize(ByteBuffer, ProtocolVersion)}). - * - * @param input The serialized form of the field. - * @param target The object to set the field on. - * @param fieldName The name of the field. Note that if it is case-sensitive or contains special - * characters, it will be double-quoted (i.e. the string will contain actual quote - * characters, as in {@code "\"foobar\""}). - * @param protocolVersion The protocol version to use. - * @return The target object with the field set. In most cases this should be the same as {@code - * target}, but if you're dealing with immutable types you'll need to return a different - * instance. - */ - protected abstract T deserializeAndSetField( - ByteBuffer input, T target, String fieldName, ProtocolVersion protocolVersion); - - /** - * Format an individual field in an object as a CQL literal, as part of formatting the whole - * object (see {@link #format(Object)}). - * - * @param source The object to read the field from. - * @param fieldName The name of the field. Note that if it is case-sensitive or contains special - * characters, it will be double-quoted (i.e. the string will contain actual quote - * characters, as in {@code "\"foobar\""}). - * @return The formatted value. - */ - protected abstract String formatField(T source, String fieldName); - - /** - * Parse an individual field and set it on an object, as part of parsing the whole object (see - * {@link #parse(String)}). - * - * @param input The String to parse the field from. - * @param target The value to write to. - * @param fieldName The name of the field. Note that if it is case-sensitive or contains special - * characters, it will be double-quoted (i.e. the string will contain actual quote - * characters, as in {@code "\"foobar\""}). - * @return The target object with the field set. In most cases this should be the same as {@code - * target}, but if you're dealing with immutable types you'll need to return a different - * instance. - */ - protected abstract T parseAndSetField(String input, T target, String fieldName); - } - - /** This codec maps a CQL {@link UserType} to a {@link UDTValue}. */ - private static class UDTCodec extends AbstractUDTCodec { - - private UDTCodec(UserType definition) { - super(definition, UDTValue.class); - } - - @Override - public boolean accepts(Object value) { - return super.accepts(value) && ((UDTValue) value).getType().equals(definition); - } - - @Override - protected UDTValue newInstance() { - return definition.newValue(); - } - - @Override - protected ByteBuffer serializeField( - UDTValue source, String fieldName, ProtocolVersion protocolVersion) { - return source.getBytesUnsafe(fieldName); - } - - @Override - protected UDTValue deserializeAndSetField( - ByteBuffer input, UDTValue target, String fieldName, ProtocolVersion protocolVersion) { - return target.setBytesUnsafe(fieldName, input); - } - - @Override - protected String formatField(UDTValue source, String fieldName) { - DataType elementType = definition.getFieldType(fieldName); - TypeCodec codec = definition.getCodecRegistry().codecFor(elementType); - return codec.format(source.get(fieldName, codec.getJavaType())); - } - - @Override - protected UDTValue parseAndSetField(String input, UDTValue target, String fieldName) { - DataType elementType = definition.getFieldType(fieldName); - TypeCodec codec = definition.getCodecRegistry().codecFor(elementType); - target.set(fieldName, codec.parse(input), codec.getJavaType()); - return target; - } - } - - /** - * Base class for codecs mapping CQL {@link TupleType tuples} to Java objects. It can serve as a - * base class for codecs dealing with direct tuple-to-Pojo mappings. - * - * @param The Java type that this codec handles. - */ - public abstract static class AbstractTupleCodec extends TypeCodec { - - protected final TupleType definition; - - protected AbstractTupleCodec(TupleType definition, Class javaClass) { - this(definition, TypeToken.of(javaClass)); - } - - protected AbstractTupleCodec(TupleType definition, TypeToken javaType) { - super(definition, javaType); - this.definition = definition; - } - - @Override - public boolean accepts(DataType cqlType) { - // a tuple codec should accept tuple values of a different type, - // provided that the latter is contained in this codec's type. - return super.accepts(cqlType) && definition.contains((TupleType) cqlType); - } - - @Override - public ByteBuffer serialize(T value, ProtocolVersion protocolVersion) { - if (value == null) return null; - int size = 0; - int length = definition.getComponentTypes().size(); - ByteBuffer[] elements = new ByteBuffer[length]; - for (int i = 0; i < length; i++) { - elements[i] = serializeField(value, i, protocolVersion); - size += 4 + (elements[i] == null ? 0 : elements[i].remaining()); - } - ByteBuffer result = ByteBuffer.allocate(size); - for (ByteBuffer bb : elements) { - if (bb == null) { - result.putInt(-1); - } else { - result.putInt(bb.remaining()); - result.put(bb.duplicate()); - } - } - return (ByteBuffer) result.flip(); - } - - @Override - public T deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) { - if (bytes == null) return null; - // empty byte buffers will result in empty values - try { - ByteBuffer input = bytes.duplicate(); - T value = newInstance(); - int i = 0; - while (input.hasRemaining() && i < definition.getComponentTypes().size()) { - int n = input.getInt(); - ByteBuffer element = n < 0 ? null : CodecUtils.readBytes(input, n); - value = deserializeAndSetField(element, value, i++, protocolVersion); - } - return value; - } catch (BufferUnderflowException e) { - throw new InvalidTypeException("Not enough bytes to deserialize a tuple", e); - } - } - - @Override - public String format(T value) { - if (value == null) return "NULL"; - StringBuilder sb = new StringBuilder("("); - int length = definition.getComponentTypes().size(); - for (int i = 0; i < length; i++) { - if (i > 0) sb.append(","); - sb.append(formatField(value, i)); - } - sb.append(")"); - return sb.toString(); - } - - @Override - public T parse(String value) { - if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; - - T v = newInstance(); - - int idx = ParseUtils.skipSpaces(value, 0); - if (value.charAt(idx++) != '(') - throw new InvalidTypeException( - String.format( - "Cannot parse tuple value from \"%s\", at character %d expecting '(' but got '%c'", - value, idx, value.charAt(idx))); - - idx = ParseUtils.skipSpaces(value, idx); - - if (value.charAt(idx) == ')') return v; - - int i = 0; - while (idx < value.length()) { - int n; - try { - n = ParseUtils.skipCQLValue(value, idx); - } catch (IllegalArgumentException e) { - throw new InvalidTypeException( - String.format( - "Cannot parse tuple value from \"%s\", invalid CQL value at character %d", - value, idx), - e); - } - - String input = value.substring(idx, n); - v = parseAndSetField(input, v, i); - idx = n; - i += 1; - - idx = ParseUtils.skipSpaces(value, idx); - if (value.charAt(idx) == ')') return v; - if (value.charAt(idx) != ',') - throw new InvalidTypeException( - String.format( - "Cannot parse tuple value from \"%s\", at character %d expecting ',' but got '%c'", - value, idx, value.charAt(idx))); - ++idx; // skip ',' - - idx = ParseUtils.skipSpaces(value, idx); - } - throw new InvalidTypeException( - String.format("Malformed tuple value \"%s\", missing closing ')'", value)); - } - - /** - * Return a new instance of {@code T}. - * - * @return A new instance of {@code T}. - */ - protected abstract T newInstance(); - - /** - * Serialize an individual field in an object, as part of serializing the whole object to a CQL - * tuple (see {@link #serialize(Object, ProtocolVersion)}). - * - * @param source The object to read the field from. - * @param index The index of the field. - * @param protocolVersion The protocol version to use. - * @return The serialized field, or {@code null} if that field should be ignored. - */ - protected abstract ByteBuffer serializeField( - T source, int index, ProtocolVersion protocolVersion); - - /** - * Deserialize an individual field and set it on an object, as part of deserializing the whole - * object from a CQL tuple (see {@link #deserialize(ByteBuffer, ProtocolVersion)}). - * - * @param input The serialized form of the field. - * @param target The object to set the field on. - * @param index The index of the field. - * @param protocolVersion The protocol version to use. - * @return The target object with the field set. In most cases this should be the same as {@code - * target}, but if you're dealing with immutable types you'll need to return a different - * instance. - */ - protected abstract T deserializeAndSetField( - ByteBuffer input, T target, int index, ProtocolVersion protocolVersion); - - /** - * Format an individual field in an object as a CQL literal, as part of formatting the whole - * object (see {@link #format(Object)}). - * - * @param source The object to read the field from. - * @param index The index of the field. - * @return The formatted value. - */ - protected abstract String formatField(T source, int index); - - /** - * Parse an individual field and set it on an object, as part of parsing the whole object (see - * {@link #parse(String)}). - * - * @param input The String to parse the field from. - * @param target The value to write to. - * @param index The index of the field. - * @return The target object with the field set. In most cases this should be the same as {@code - * target}, but if you're dealing with immutable types you'll need to return a different - * instance. - */ - protected abstract T parseAndSetField(String input, T target, int index); - } - - /** This codec maps a CQL {@link TupleType tuple} to a {@link TupleValue}. */ - private static class TupleCodec extends AbstractTupleCodec { - - private TupleCodec(TupleType definition) { - super(definition, TupleValue.class); - } - - @Override - public boolean accepts(Object value) { - // a tuple codec should accept tuple values of a different type, - // provided that the latter is contained in this codec's type. - return super.accepts(value) && definition.contains(((TupleValue) value).getType()); - } - - @Override - protected TupleValue newInstance() { - return definition.newValue(); - } - - @Override - protected ByteBuffer serializeField( - TupleValue source, int index, ProtocolVersion protocolVersion) { - if (index >= source.values.length) return null; - return source.getBytesUnsafe(index); - } - - @Override - protected TupleValue deserializeAndSetField( - ByteBuffer input, TupleValue target, int index, ProtocolVersion protocolVersion) { - if (index >= target.values.length) return target; - return target.setBytesUnsafe(index, input); - } - - @Override - protected String formatField(TupleValue value, int index) { - DataType elementType = definition.getComponentTypes().get(index); - TypeCodec codec = definition.getCodecRegistry().codecFor(elementType); - return codec.format(value.get(index, codec.getJavaType())); - } - - @Override - protected TupleValue parseAndSetField(String input, TupleValue target, int index) { - DataType elementType = definition.getComponentTypes().get(index); - TypeCodec codec = definition.getCodecRegistry().codecFor(elementType); - target.set(index, codec.parse(input), codec.getJavaType()); - return target; - } - } - - private static class DurationCodec extends TypeCodec { - - private static final DurationCodec instance = new DurationCodec(); - - private DurationCodec() { - super(DataType.duration(), Duration.class); - } - - @Override - public ByteBuffer serialize(Duration duration, ProtocolVersion protocolVersion) - throws InvalidTypeException { - if (duration == null) return null; - long months = duration.getMonths(); - long days = duration.getDays(); - long nanoseconds = duration.getNanoseconds(); - int size = - VIntCoding.computeVIntSize(months) - + VIntCoding.computeVIntSize(days) - + VIntCoding.computeVIntSize(nanoseconds); - ByteArrayDataOutput out = ByteStreams.newDataOutput(size); - try { - VIntCoding.writeVInt(months, out); - VIntCoding.writeVInt(days, out); - VIntCoding.writeVInt(nanoseconds, out); - } catch (IOException e) { - // cannot happen - throw new AssertionError(); - } - return ByteBuffer.wrap(out.toByteArray()); - } - - @Override - public Duration deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) - throws InvalidTypeException { - if (bytes == null || bytes.remaining() == 0) { - return null; - } else { - DataInput in = ByteStreams.newDataInput(Bytes.getArray(bytes)); - try { - int months = (int) VIntCoding.readVInt(in); - int days = (int) VIntCoding.readVInt(in); - long nanoseconds = VIntCoding.readVInt(in); - return Duration.newInstance(months, days, nanoseconds); - } catch (IOException e) { - // cannot happen - throw new AssertionError(); - } - } - } - - @Override - public Duration parse(String value) throws InvalidTypeException { - if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) return null; - return Duration.from(value); - } - - @Override - public String format(Duration value) throws InvalidTypeException { - if (value == null) return "NULL"; - return value.toString(); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/TypeTokens.java b/driver-core/src/main/java/com/datastax/driver/core/TypeTokens.java deleted file mode 100644 index c912b096332..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/TypeTokens.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.reflect.TypeParameter; -import com.google.common.reflect.TypeToken; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** Utility methods to create {@code TypeToken} instances. */ -public final class TypeTokens { - private TypeTokens() {} - - /** - * Create a {@link TypeToken} that represents a {@link List} whose elements are of the given type. - * - * @param eltType The list element type. - * @param The list element type. - * @return A {@link TypeToken} that represents a {@link List} whose elements are of the given - * type. - */ - public static TypeToken> listOf(Class eltType) { - // @formatter:off - return new TypeToken>() {}.where(new TypeParameter() {}, eltType); - // @formatter:on - } - - /** - * Create a {@link TypeToken} that represents a {@link List} whose elements are of the given type. - * - * @param eltType The list element type. - * @param The list element type. - * @return A {@link TypeToken} that represents a {@link List} whose elements are of the given - * type. - */ - public static TypeToken> listOf(TypeToken eltType) { - // @formatter:off - return new TypeToken>() {}.where(new TypeParameter() {}, eltType); - // @formatter:on - } - - /** - * Create a {@link TypeToken} that represents a {@link Set} whose elements are of the given type. - * - * @param eltType The set element type. - * @param The set element type. - * @return A {@link TypeToken} that represents a {@link Set} whose elements are of the given type. - */ - public static TypeToken> setOf(Class eltType) { - // @formatter:off - return new TypeToken>() {}.where(new TypeParameter() {}, eltType); - // @formatter:on - } - - /** - * Create a {@link TypeToken} that represents a {@link Set} whose elements are of the given type. - * - * @param eltType The set element type. - * @param The set element type. - * @return A {@link TypeToken} that represents a {@link Set} whose elements are of the given type. - */ - public static TypeToken> setOf(TypeToken eltType) { - // @formatter:off - return new TypeToken>() {}.where(new TypeParameter() {}, eltType); - // @formatter:on - } - - /** - * Create a {@link TypeToken} that represents a {@link Map} whose keys and values are of the given - * key and value types. - * - * @param keyType The map key type. - * @param valueType The map value type - * @param The map key type. - * @param The map value type - * @return A {@link TypeToken} that represents a {@link Map} whose keys and values are of the - * given key and value types - */ - public static TypeToken> mapOf(Class keyType, Class valueType) { - // @formatter:off - return new TypeToken>() {}.where(new TypeParameter() {}, keyType) - .where(new TypeParameter() {}, valueType); - // @formatter:on - } - - /** - * Create a {@link TypeToken} that represents a {@link Map} whose keys and values are of the given - * key and value types. - * - * @param keyType The map key type. - * @param valueType The map value type - * @param The map key type. - * @param The map value type - * @return A {@link TypeToken} that represents a {@link Map} whose keys and values are of the - * given key and value types - */ - public static TypeToken> mapOf(TypeToken keyType, TypeToken valueType) { - // @formatter:off - return new TypeToken>() {}.where(new TypeParameter() {}, keyType) - .where(new TypeParameter() {}, valueType); - // @formatter:on - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/UDTValue.java b/driver-core/src/main/java/com/datastax/driver/core/UDTValue.java deleted file mode 100644 index a1b9e196222..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/UDTValue.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** A value for a User Defined Type. */ -public class UDTValue extends AbstractData { - - private final UserType definition; - - UDTValue(UserType definition) { - super(definition.getProtocolVersion(), definition.size()); - this.definition = definition; - } - - @Override - protected DataType getType(int i) { - return definition.byIdx[i].getType(); - } - - @Override - protected String getName(int i) { - return definition.byIdx[i].getName(); - } - - @Override - protected CodecRegistry getCodecRegistry() { - return definition.getCodecRegistry(); - } - - @Override - protected int[] getAllIndexesOf(String name) { - int[] indexes = definition.byName.get(Metadata.handleId(name)); - if (indexes == null) - throw new IllegalArgumentException(name + " is not a field defined in this UDT"); - return indexes; - } - - /** - * The UDT this is a value of. - * - * @return the UDT this is a value of. - */ - public UserType getType() { - return definition; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof UDTValue)) return false; - - UDTValue that = (UDTValue) o; - if (!definition.equals(that.definition)) return false; - - return super.equals(o); - } - - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - TypeCodec codec = getCodecRegistry().codecFor(definition); - sb.append(codec.format(this)); - return sb.toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/UserType.java b/driver-core/src/main/java/com/datastax/driver/core/UserType.java deleted file mode 100644 index 1683b7b022c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/UserType.java +++ /dev/null @@ -1,413 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterators; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * A User Defined Type (UDT). - * - *

A UDT is a essentially a named collection of fields (with a name and a type). - */ -public class UserType extends DataType implements Iterable { - - static final String TYPE_NAME = "type_name"; - private static final String COLS_NAMES = "field_names"; - static final String COLS_TYPES = "field_types"; - - private final String keyspace; - private final String typeName; - private final boolean frozen; - private final ProtocolVersion protocolVersion; - - // can be null, if this object is being constructed from a response message - // see Responses.Result.Rows.Metadata.decode() - private volatile CodecRegistry codecRegistry; - - // Note that we don't expose the order of fields, from an API perspective this is a map - // of String->Field, but internally we care about the order because the serialization format - // of UDT expects a particular order. - final Field[] byIdx; - // For a given name, we can only have one field with that name, so we don't need a int[] in - // practice. However, storing one element arrays save allocations in UDTValue.getAllIndexesOf - // implementation. - final Map byName; - - private UserType( - Name name, - String keyspace, - String typeName, - boolean frozen, - ProtocolVersion protocolVersion, - CodecRegistry codecRegistry, - Field[] byIdx, - Map byName) { - super(name); - this.keyspace = keyspace; - this.typeName = typeName; - this.frozen = frozen; - this.protocolVersion = protocolVersion; - this.codecRegistry = codecRegistry; - this.byIdx = byIdx; - this.byName = byName; - } - - UserType( - String keyspace, - String typeName, - boolean frozen, - Collection fields, - ProtocolVersion protocolVersion, - CodecRegistry codecRegistry) { - this( - DataType.Name.UDT, - keyspace, - typeName, - frozen, - protocolVersion, - codecRegistry, - fields.toArray(new Field[fields.size()]), - mapByName(fields)); - } - - private static ImmutableMap mapByName(Collection fields) { - ImmutableMap.Builder builder = new ImmutableMap.Builder(); - int i = 0; - for (Field field : fields) { - builder.put(field.getName(), new int[] {i}); - i += 1; - } - return builder.build(); - } - - static UserType build( - KeyspaceMetadata ksm, - Row row, - VersionNumber version, - Cluster cluster, - Map userTypes) { - ProtocolVersion protocolVersion = - cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - CodecRegistry codecRegistry = cluster.getConfiguration().getCodecRegistry(); - - String keyspace = row.getString(KeyspaceMetadata.KS_NAME); - String name = row.getString(TYPE_NAME); - - List fieldNames = row.getList(COLS_NAMES, String.class); - List fieldTypes = row.getList(COLS_TYPES, String.class); - - List fields = new ArrayList(fieldNames.size()); - for (int i = 0; i < fieldNames.size(); i++) { - DataType fieldType; - if (version.getMajor() >= 3.0) { - fieldType = - DataTypeCqlNameParser.parse( - fieldTypes.get(i), cluster, ksm.getName(), userTypes, ksm.userTypes, false, false); - } else { - fieldType = - DataTypeClassNameParser.parseOne(fieldTypes.get(i), protocolVersion, codecRegistry); - } - fields.add(new Field(fieldNames.get(i), fieldType)); - } - return new UserType(keyspace, name, false, fields, protocolVersion, codecRegistry); - } - - /** - * Returns a new empty value for this user type definition. - * - * @return an empty value for this user type definition. - */ - public UDTValue newValue() { - return new UDTValue(this); - } - - /** - * The name of the keyspace this UDT is part of. - * - * @return the name of the keyspace this UDT is part of. - */ - public String getKeyspace() { - return keyspace; - } - - /** - * The name of this user type. - * - * @return the name of this user type. - */ - public String getTypeName() { - return typeName; - } - - /** - * Returns the number of fields in this UDT. - * - * @return the number of fields in this UDT. - */ - public int size() { - return byIdx.length; - } - - /** - * Returns whether this UDT contains a given field. - * - * @param name the name to check. Note that {@code name} obey the usual CQL identifier rules: it - * should be quoted if it denotes a case sensitive identifier (you can use {@link - * Metadata#quote} for the quoting). - * @return {@code true} if this UDT contains a field named {@code name}, {@code false} otherwise. - */ - public boolean contains(String name) { - return byName.containsKey(Metadata.handleId(name)); - } - - /** - * Returns an iterator over the fields of this UDT. - * - * @return an iterator over the fields of this UDT. - */ - @Override - public Iterator iterator() { - return Iterators.forArray(byIdx); - } - - /** - * Returns the names of the fields of this UDT. - * - * @return the names of the fields of this UDT as a collection. - */ - public Collection getFieldNames() { - return byName.keySet(); - } - - /** - * Returns the type of a given field. - * - * @param name the name of the field. Note that {@code name} obey the usual CQL identifier rules: - * it should be quoted if it denotes a case sensitive identifier (you can use {@link - * Metadata#quote} for the quoting). - * @return the type of field {@code name} if this UDT has a field of this name, {@code null} - * otherwise. - * @throws IllegalArgumentException if {@code name} is not a field of this UDT definition. - */ - public DataType getFieldType(String name) { - int[] idx = byName.get(Metadata.handleId(name)); - if (idx == null) - throw new IllegalArgumentException(name + " is not a field defined in this definition"); - - return byIdx[idx[0]].getType(); - } - - @Override - public boolean isFrozen() { - return frozen; - } - - public UserType copy(boolean newFrozen) { - if (newFrozen == frozen) { - return this; - } else { - return new UserType( - name, keyspace, typeName, newFrozen, protocolVersion, codecRegistry, byIdx, byName); - } - } - - @Override - public int hashCode() { - int result = name.hashCode(); - result = 31 * result + keyspace.hashCode(); - result = 31 * result + typeName.hashCode(); - result = 31 * result + Arrays.hashCode(byIdx); - return result; - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof UserType)) return false; - - UserType other = (UserType) o; - - // Note: we don't test byName because it's redundant with byIdx in practice, - // but also because the map holds 'int[]' which don't have proper equal. - return name.equals(other.name) - && keyspace.equals(other.keyspace) - && typeName.equals(other.typeName) - && Arrays.equals(byIdx, other.byIdx); - } - - /** - * Returns a CQL query representing this user type in human readable form. - * - *

This method is equivalent to {@link #asCQLQuery} but the ouptut is formatted to be human - * readable (for some definition of human readable). - * - * @return the CQL query representing this user type. - */ - public String exportAsString() { - return asCQLQuery(true); - } - - /** - * Returns a CQL query representing this user type. - * - *

This method returns a single 'CREATE TYPE' query corresponding to this UDT definition. - * - *

Note that the returned string is a single line; the returned query is not formatted in any - * way. - * - * @return the 'CREATE TYPE' query corresponding to this user type. - * @see #exportAsString - */ - public String asCQLQuery() { - return asCQLQuery(false); - } - - /** - * Return the protocol version that has been used to deserialize this UDT, or that will be used to - * serialize it. In most cases this should be the version currently in use by the cluster instance - * that this UDT belongs to, as reported by {@link ProtocolOptions#getProtocolVersion()}. - * - * @return the protocol version that has been used to deserialize this UDT, or that will be used - * to serialize it. - */ - ProtocolVersion getProtocolVersion() { - return protocolVersion; - } - - CodecRegistry getCodecRegistry() { - return codecRegistry; - } - - void setCodecRegistry(CodecRegistry codecRegistry) { - this.codecRegistry = codecRegistry; - } - - private String asCQLQuery(boolean formatted) { - StringBuilder sb = new StringBuilder(); - - sb.append("CREATE TYPE ") - .append(Metadata.quoteIfNecessary(keyspace)) - .append('.') - .append(Metadata.quoteIfNecessary(typeName)) - .append(" ("); - if (formatted) { - TableMetadata.spaceOrNewLine(sb, true); - } - for (int i = 0; i < byIdx.length; i++) { - sb.append(byIdx[i]); - if (i < byIdx.length - 1) { - sb.append(','); - TableMetadata.spaceOrNewLine(sb, formatted); - } else { - TableMetadata.newLine(sb, formatted); - } - } - - return sb.append(");").toString(); - } - - @Override - public String toString() { - String str = - Metadata.quoteIfNecessary(getKeyspace()) + "." + Metadata.quoteIfNecessary(getTypeName()); - return isFrozen() ? "frozen<" + str + ">" : str; - } - - @Override - public String asFunctionParameterString() { - return Metadata.quoteIfNecessary(getTypeName()); - } - - /** A UDT field. */ - public static class Field { - private final String name; - private final DataType type; - - Field(String name, DataType type) { - this.name = name; - this.type = type; - } - - /** - * Returns the name of the field. - * - * @return the name of the field. - */ - public String getName() { - return name; - } - - /** - * Returns the type of the field. - * - * @return the type of the field. - */ - public DataType getType() { - return type; - } - - @Override - public final int hashCode() { - return Arrays.hashCode(new Object[] {name, type}); - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof Field)) return false; - - Field other = (Field) o; - return name.equals(other.name) && type.equals(other.type); - } - - @Override - public String toString() { - return Metadata.quoteIfNecessary(name) + ' ' + type; - } - } - - /** - * A "shallow" definition of a UDT that only contains the keyspace and type name, without any - * information about the type's structure. - * - *

This is used for internal dependency analysis only, and never returned to the client. - * - * @since 3.0.0 - */ - static class Shallow extends DataType { - - final String keyspaceName; - final String typeName; - final boolean frozen; - - Shallow(String keyspaceName, String typeName, boolean frozen) { - super(Name.UDT); - this.keyspaceName = keyspaceName; - this.typeName = typeName; - this.frozen = frozen; - } - - @Override - public boolean isFrozen() { - return frozen; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/VersionNumber.java b/driver-core/src/main/java/com/datastax/driver/core/VersionNumber.java deleted file mode 100644 index 58e9ef47a8c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/VersionNumber.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -import com.datastax.driver.core.utils.MoreObjects; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A version number in the form X.Y.Z with optional pre-release labels and build metadata. - * - *

Version numbers compare the usual way, the major number (X) is compared first, then the minor - * one (Y) and then the patch level one (Z). Lastly, versions with pre-release sorts before the - * versions that don't have one, and labels are sorted alphabetically if necessary. Build metadata - * are ignored for sorting versions. The versions supported loosely correspond to what - * http://semver.org/ defines though it does not adhere to it strictly. - */ -public class VersionNumber implements Comparable { - - private static final String VERSION_REGEXP = - "(\\d+)\\.(\\d+)(\\.\\d+)?(\\.\\d+)?([~\\-]\\w[.\\w]*(?:\\-\\w[.\\w]*)*)?(\\+[.\\w]+)?"; - private static final Pattern pattern = Pattern.compile(VERSION_REGEXP); - - private final int major; - private final int minor; - private final int patch; - private final int dsePatch; - - private final String[] preReleases; - private final String build; - - private VersionNumber( - int major, int minor, int patch, int dsePatch, String[] preReleases, String build) { - this.major = major; - this.minor = minor; - this.patch = patch; - this.dsePatch = dsePatch; - this.preReleases = preReleases; - this.build = build; - } - - /** - * Parse a version from a string. - * - *

The version string should have primarily the form X.Y.Z to which can be appended one or more - * pre-release label after dashes (2.0.1-beta1, 2.1.4-rc1-SNAPSHOT) and an optional build label - * (2.1.0-beta1+a20ba.sha). Out of convenience, the "patch" version number, Z, can be omitted, in - * which case it is assumed to be 0. - * - * @param version the string to parse - * @return the parsed version number. - * @throws IllegalArgumentException if the provided string does not represent a valid version. - */ - public static VersionNumber parse(String version) { - if (version == null) return null; - - Matcher matcher = pattern.matcher(version); - if (!matcher.matches()) - throw new IllegalArgumentException("Invalid version number: " + version); - - try { - int major = Integer.parseInt(matcher.group(1)); - int minor = Integer.parseInt(matcher.group(2)); - - String pa = matcher.group(3); - int patch = - pa == null || pa.isEmpty() - ? 0 - : Integer.parseInt( - pa.substring(1)); // dropping the initial '.' since it's included this time - - String dse = matcher.group(4); - int dsePatch = - dse == null || dse.isEmpty() - ? -1 - : Integer.parseInt( - dse.substring(1)); // dropping the initial '.' since it's included this time - - String pr = matcher.group(5); - String[] preReleases = - pr == null || pr.isEmpty() - ? null - : pr.substring(1) - .split("\\-"); // drop initial '-' or '~' then split on the remaining ones - - String bl = matcher.group(6); - String build = bl == null || bl.isEmpty() ? null : bl.substring(1); // drop the initial '+' - - return new VersionNumber(major, minor, patch, dsePatch, preReleases, build); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid version number: " + version); - } - } - - /** - * The major version number. - * - * @return the major version number, i.e. X in X.Y.Z. - */ - public int getMajor() { - return major; - } - - /** - * The minor version number. - * - * @return the minor version number, i.e. Y in X.Y.Z. - */ - public int getMinor() { - return minor; - } - - /** - * The patch version number. - * - * @return the patch version number, i.e. Z in X.Y.Z. - */ - public int getPatch() { - return patch; - } - - /** - * The DSE patch version number (will only be present for version of Cassandra in DSE). - * - *

DataStax Entreprise (DSE) adds a fourth number to the version number to track potential hot - * fixes and/or DSE specific patches that may have been applied to the Cassandra version. In that - * case, this method return that fourth number. - * - * @return the DSE patch version number, i.e. D in X.Y.Z.D, or -1 if the version number is not - * from DSE. - */ - public int getDSEPatch() { - return dsePatch; - } - - /** - * The pre-release labels if relevant, i.e. label1 and label2 in X.Y.Z-label1-lable2. - * - * @return the pre-releases labels. The return list will be {@code null} if the version number - * doesn't have one. - */ - public List getPreReleaseLabels() { - return preReleases == null ? null : Collections.unmodifiableList(Arrays.asList(preReleases)); - } - - /** - * The build label if there is one. - * - * @return the build label or {@code null} if the version number doesn't have one. - */ - public String getBuildLabel() { - return build; - } - - /** - * The next stable version, i.e. the version stripped of its pre-release labels and build - * metadata. - * - *

This is mostly used during our development stage, where we test the driver against - * pre-release versions of Cassandra like 2.1.0-rc7-SNAPSHOT, but need to compare to the stable - * version 2.1.0 when testing for native protocol compatibility, etc. - * - * @return the next stable version. - */ - public VersionNumber nextStable() { - return new VersionNumber(major, minor, patch, dsePatch, null, null); - } - - @Override - public int compareTo(VersionNumber other) { - if (major < other.major) return -1; - if (major > other.major) return 1; - - if (minor < other.minor) return -1; - if (minor > other.minor) return 1; - - if (patch < other.patch) return -1; - if (patch > other.patch) return 1; - - if (dsePatch < 0) { - if (other.dsePatch >= 0) return -1; - } else { - if (other.dsePatch < 0) return 1; - - // Both are >= 0 - if (dsePatch < other.dsePatch) return -1; - if (dsePatch > other.dsePatch) return 1; - } - - if (preReleases == null) return other.preReleases == null ? 0 : 1; - if (other.preReleases == null) return -1; - - for (int i = 0; i < Math.min(preReleases.length, other.preReleases.length); i++) { - int cmp = preReleases[i].compareTo(other.preReleases[i]); - if (cmp != 0) return cmp; - } - - return preReleases.length == other.preReleases.length - ? 0 - : (preReleases.length < other.preReleases.length ? -1 : 1); - } - - @Override - public boolean equals(Object other) { - if (other == this) return true; - if (!(other instanceof VersionNumber)) return false; - VersionNumber that = (VersionNumber) other; - return this.major == that.major - && this.minor == that.minor - && this.patch == that.patch - && this.dsePatch == that.dsePatch - && (this.preReleases == null - ? that.preReleases == null - : Arrays.equals(this.preReleases, that.preReleases)) - && MoreObjects.equal(this.build, that.build); - } - - @Override - public int hashCode() { - return MoreObjects.hashCode(major, minor, patch, dsePatch, Arrays.hashCode(preReleases), build); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(major).append('.').append(minor).append('.').append(patch); - if (dsePatch >= 0) sb.append('.').append(dsePatch); - if (preReleases != null) { - for (String preRelease : preReleases) sb.append('-').append(preRelease); - } - if (build != null) sb.append('+').append(build); - return sb.toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/WriteType.java b/driver-core/src/main/java/com/datastax/driver/core/WriteType.java deleted file mode 100644 index c16e3ec44cf..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/WriteType.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core; - -/** - * The type of a Cassandra write query. - * - *

This information is returned by Cassandra when a write timeout is raised to indicate what type - * of write timed out. This information is useful to decide which retry policy to adopt. - */ -public enum WriteType { - /** A write to a single partition key. Such writes are guaranteed to be atomic and isolated. */ - SIMPLE, - /** - * A write to a multiple partition key that used the distributed batch log to ensure atomicity - * (atomicity meaning that if any statement in the batch succeeds, all will eventually succeed). - */ - BATCH, - /** - * A write to a multiple partition key that doesn't use the distributed batch log. Atomicity for - * such writes is not guaranteed - */ - UNLOGGED_BATCH, - /** - * A counter write (that can be for one or multiple partition key). Such write should not be - * replayed to avoid over-counting. - */ - COUNTER, - /** - * The initial write to the distributed batch log that Cassandra performs internally before a - * BATCH write. - */ - BATCH_LOG, - /** - * A conditional write. If a timeout has this {@code WriteType}, the timeout has happened while - * doing the compare-and-swap for an conditional update. In this case, the update may or may not - * have been applied. - */ - CAS, - /** - * Indicates that the timeout was related to acquiring locks needed for updating materialized - * views affected by write operation. - */ - VIEW, - /** - * Indicates that the timeout was related to acquiring space for change data capture logs for cdc - * tracked tables. - */ - CDC; -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/AlreadyExistsException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/AlreadyExistsException.java deleted file mode 100644 index b3d3577cb01..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/AlreadyExistsException.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Exception thrown when a query attempts to create a keyspace or table that already exists. */ -public class AlreadyExistsException extends QueryValidationException - implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - private final String keyspace; - private final String table; - - public AlreadyExistsException(String keyspace, String table) { - this(null, keyspace, table); - } - - public AlreadyExistsException(EndPoint endPoint, String keyspace, String table) { - super(makeMsg(keyspace, table)); - this.endPoint = endPoint; - this.keyspace = keyspace; - this.table = table; - } - - private AlreadyExistsException( - EndPoint endPoint, String msg, Throwable cause, String keyspace, String table) { - super(msg, cause); - this.endPoint = endPoint; - this.keyspace = keyspace; - this.table = table; - } - - private static String makeMsg(String keyspace, String table) { - if (table.isEmpty()) return String.format("Keyspace %s already exists", keyspace); - else return String.format("Table %s.%s already exists", keyspace, table); - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - /** - * Returns whether the query yielding this exception was a table creation attempt. - * - * @return {@code true} if this exception is raised following a table creation attempt, {@code - * false} if it was a keyspace creation attempt. - */ - public boolean wasTableCreation() { - return !table.isEmpty(); - } - - /** - * The name of keyspace that either already exists or is home to the table that already exists. - * - * @return a keyspace name that is either the keyspace whose creation attempt failed because a - * keyspace of the same name already exists (in that case, {@link #table} will return {@code - * null}), or the keyspace of the table creation attempt (in which case {@link #table} will - * return the name of said table). - */ - public String getKeyspace() { - return keyspace; - } - - /** - * If the failed creation was a table creation, the name of the table that already exists. - * - * @return the name of table whose creation attempt failed because a table of this name already - * exists, or {@code null} if the query was a keyspace creation query. - */ - public String getTable() { - return table.isEmpty() ? null : table; - } - - @Override - public DriverException copy() { - return new AlreadyExistsException(getEndPoint(), getMessage(), this, keyspace, table); - } - - /** - * Create a copy of this exception with a nicer stack trace, and including the coordinator address - * that caused this exception to be raised. - * - *

This method is mainly intended for internal use by the driver and exists mainly because: - * - *

    - *
  1. the original exception was decoded from a response frame and at that time, the - * coordinator address was not available; and - *
  2. the newly-created exception will refer to the current thread in its stack trace, which - * generally yields a more user-friendly stack trace that the original one. - *
- * - * @param endPoint The full address of the host that caused this exception to be thrown. - * @return a copy/clone of this exception, but with the given host address instead of the original - * one. - */ - public AlreadyExistsException copy(EndPoint endPoint) { - return new AlreadyExistsException(endPoint, getMessage(), this, keyspace, table); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/AuthenticationException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/AuthenticationException.java deleted file mode 100644 index 5b1f6bb3c3b..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/AuthenticationException.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Indicates an error during the authentication phase while connecting to a node. */ -public class AuthenticationException extends DriverException implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public AuthenticationException(EndPoint endPoint, String message) { - super(String.format("Authentication error on host %s: %s", endPoint, message)); - this.endPoint = endPoint; - } - - // Preserve a constructor with InetSocketAddress for backward compatibility, because legacy - // authenticators might use it - public AuthenticationException(InetSocketAddress address, String message) { - this(new WrappingEndPoint(address), message); - } - - private AuthenticationException(EndPoint endPoint, String message, Throwable cause) { - super(message, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public DriverException copy() { - return new AuthenticationException(endPoint, getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/BootstrappingException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/BootstrappingException.java deleted file mode 100644 index 8c5aab0ba3a..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/BootstrappingException.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Indicates that the contacted host was bootstrapping when it received a read query. */ -public class BootstrappingException extends QueryExecutionException - implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public BootstrappingException(EndPoint endPoint, String message) { - super(String.format("Queried host (%s) was bootstrapping: %s", endPoint, message)); - this.endPoint = endPoint; - } - - /** Private constructor used solely when copying exceptions. */ - private BootstrappingException(EndPoint endPoint, String message, BootstrappingException cause) { - super(message, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public BootstrappingException copy() { - return new BootstrappingException(endPoint, getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/BusyConnectionException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/BusyConnectionException.java deleted file mode 100644 index a8bfd67f77a..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/BusyConnectionException.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Indicates that a connection has run out of stream IDs. */ -public class BusyConnectionException extends DriverException implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public BusyConnectionException(EndPoint endPoint) { - super(String.format("[%s] Connection has run out of stream IDs", endPoint)); - this.endPoint = endPoint; - } - - public BusyConnectionException(EndPoint endPoint, Throwable cause) { - super(String.format("[%s] Connection has run out of stream IDs", endPoint), cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public BusyConnectionException copy() { - return new BusyConnectionException(endPoint, this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/BusyPoolException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/BusyPoolException.java deleted file mode 100644 index 94f89b5c0a1..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/BusyPoolException.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import com.datastax.driver.core.HostDistance; -import com.datastax.driver.core.Statement; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.util.concurrent.TimeUnit; - -/** - * Indicates that a connection pool has run out of available connections. - * - *

This happens if the pool has no connections (for example if it's currently reconnecting to its - * host), or if all connections have reached their maximum number of in flight queries. The query - * will be retried on the next host in the {@link - * com.datastax.driver.core.policies.LoadBalancingPolicy#newQueryPlan(String, Statement) query - * plan}. - * - *

This exception is a symptom that the driver is experiencing a high workload. If it happens - * regularly on all hosts, you should consider tuning one (or a combination of) the following - * pooling options: - * - *

    - *
  • {@link com.datastax.driver.core.PoolingOptions#setMaxRequestsPerConnection(HostDistance, - * int)}: maximum number of requests per connection; - *
  • {@link com.datastax.driver.core.PoolingOptions#setMaxConnectionsPerHost(HostDistance, - * int)}: maximum number of connections in the pool; - *
  • {@link com.datastax.driver.core.PoolingOptions#setMaxQueueSize(int)}: maximum number of - * enqueued requests before this exception is thrown. - *
- */ -public class BusyPoolException extends DriverException implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public BusyPoolException(EndPoint endPoint, int queueSize) { - this(endPoint, buildMessage(endPoint, queueSize), null); - } - - public BusyPoolException(EndPoint endPoint, long timeout, TimeUnit unit) { - this(endPoint, buildMessage(endPoint, timeout, unit), null); - } - - private BusyPoolException(EndPoint endPoint, String message, Throwable cause) { - super(message, cause); - this.endPoint = endPoint; - } - - private static String buildMessage(EndPoint endPoint, int queueSize) { - return String.format( - "[%s] Pool is busy (no available connection and the queue has reached its max size %d)", - endPoint, queueSize); - } - - private static String buildMessage(EndPoint endPoint, long timeout, TimeUnit unit) { - return String.format( - "[%s] Pool is busy (no available connection and timed out after %d %s)", - endPoint, timeout, unit); - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public BusyPoolException copy() { - return new BusyPoolException(endPoint, getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/CASWriteUnknownException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/CASWriteUnknownException.java deleted file mode 100644 index 751a403991d..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/CASWriteUnknownException.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.EndPoint; - -public class CASWriteUnknownException extends QueryConsistencyException { - - private static final long serialVersionUID = 0; - - /** - * This constructor should only be used internally by the driver when decoding error responses. - */ - public CASWriteUnknownException(ConsistencyLevel consistency, int received, int required) { - this(null, consistency, received, required); - } - - public CASWriteUnknownException( - EndPoint endPoint, ConsistencyLevel consistency, int received, int required) { - super( - endPoint, - String.format( - "CAS operation result is unknown - proposal was not accepted by a quorum. (%d / %d)", - received, required), - consistency, - received, - required); - } - - private CASWriteUnknownException( - EndPoint endPoint, - String msg, - Throwable cause, - ConsistencyLevel consistency, - int received, - int required) { - super(endPoint, msg, cause, consistency, received, required); - } - - @Override - public CASWriteUnknownException copy() { - return new CASWriteUnknownException( - getEndPoint(), - getMessage(), - this, - getConsistencyLevel(), - getReceivedAcknowledgements(), - getRequiredAcknowledgements()); - } - - /** - * Create a copy of this exception with a nicer stack trace, and including the coordinator address - * that caused this exception to be raised. - * - *

This method is mainly intended for internal use by the driver and exists mainly because: - * - *

    - *
  1. the original exception was decoded from a response frame and at that time, the - * coordinator address was not available; and - *
  2. the newly-created exception will refer to the current thread in its stack trace, which - * generally yields a more user-friendly stack trace that the original one. - *
- * - * @param endPoint The full address of the host that caused this exception to be thrown. - * @return a copy/clone of this exception, but with the given host address instead of the original - * one. - */ - public CASWriteUnknownException copy(EndPoint endPoint) { - return new CASWriteUnknownException( - endPoint, - getMessage(), - this, - getConsistencyLevel(), - getReceivedAcknowledgements(), - getRequiredAcknowledgements()); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/CDCWriteException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/CDCWriteException.java deleted file mode 100644 index f4ac21c1663..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/CDCWriteException.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** An error occurred when trying to write a CDC mutation to the commitlog * */ -public class CDCWriteException extends QueryExecutionException implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public CDCWriteException(EndPoint endPoint, String message) { - super(message); - this.endPoint = endPoint; - } - - /** Private constructor used solely when copying exceptions. */ - private CDCWriteException(EndPoint endPoint, String message, CDCWriteException cause) { - super(message, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public CDCWriteException copy() { - return new CDCWriteException(endPoint, getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/CodecNotFoundException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/CodecNotFoundException.java deleted file mode 100644 index 73fb3745257..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/CodecNotFoundException.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.DataType; -import com.google.common.reflect.TypeToken; - -/** - * Thrown when a suitable {@link com.datastax.driver.core.TypeCodec} cannot be found by {@link - * com.datastax.driver.core.CodecRegistry} instances. - */ -@SuppressWarnings("serial") -public class CodecNotFoundException extends DriverException { - - private final DataType cqlType; - - private final TypeToken javaType; - - public CodecNotFoundException(String msg, DataType cqlType, TypeToken javaType) { - this(msg, null, cqlType, javaType); - } - - public CodecNotFoundException(Throwable cause, DataType cqlType, TypeToken javaType) { - this(null, cause, cqlType, javaType); - } - - private CodecNotFoundException( - String msg, Throwable cause, DataType cqlType, TypeToken javaType) { - super(msg, cause); - this.cqlType = cqlType; - this.javaType = javaType; - } - - public DataType getCqlType() { - return cqlType; - } - - public TypeToken getJavaType() { - return javaType; - } - - @Override - public CodecNotFoundException copy() { - return new CodecNotFoundException(getMessage(), getCause(), getCqlType(), getJavaType()); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/ConnectionException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/ConnectionException.java deleted file mode 100644 index 773eea6da63..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/ConnectionException.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Indicates that a connection to a host has encountered a problem and that it should be closed. */ -public class ConnectionException extends DriverException implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public ConnectionException(EndPoint endPoint, String msg, Throwable cause) { - super(msg, cause); - this.endPoint = endPoint; - } - - public ConnectionException(EndPoint endPoint, String msg) { - super(msg); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public String getMessage() { - return endPoint == null ? getRawMessage() : String.format("[%s] %s", endPoint, getRawMessage()); - } - - @Override - public ConnectionException copy() { - return new ConnectionException(endPoint, getRawMessage(), this); - } - - String getRawMessage() { - return super.getMessage(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/CoordinatorException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/CoordinatorException.java deleted file mode 100644 index 20fd93a3a3e..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/CoordinatorException.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** - * An interface for exceptions that are able to report the address of the coordinator host that was - * contacted. - */ -public interface CoordinatorException { - - /** - * The connection information of the coordinator host that was contacted. May be {@code null} if - * the coordinator is not known. - */ - EndPoint getEndPoint(); - - /** - * The coordinator host that was contacted; may be {@code null} if the coordinator is not known. - * - * @deprecated {@link #getEndPoint()} provides more accurate information if the connection - * information consists of more than a socket address. This method is a shortcut for {@code - * getEndPoint().resolve().getAddress()}. - */ - @Deprecated - InetAddress getHost(); - - /** - * The full address of the coordinator host that was contacted; may be {@code null} if the - * coordinator is not known. - * - * @deprecated {@link #getEndPoint()} provides more accurate information if the connection - * information consists of more than a socket address. This method is a shortcut for {@code - * getEndPoint().resolve()}. - */ - @Deprecated - InetSocketAddress getAddress(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/CrcMismatchException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/CrcMismatchException.java deleted file mode 100644 index a79a49fdd54..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/CrcMismatchException.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -/** - * Thrown when the checksums in a server response don't match (protocol v5 or above). - * - *

This indicates a data corruption issue, either due to a hardware issue on the client, or on - * the network between the server and the client. It is not recoverable: the driver will drop the - * connection. - */ -public class CrcMismatchException extends DriverException { - - private static final long serialVersionUID = 0; - - public CrcMismatchException(String message) { - super(message); - } - - public CrcMismatchException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public CrcMismatchException copy() { - return new CrcMismatchException(getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/DriverException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/DriverException.java deleted file mode 100644 index f8ed67b5931..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/DriverException.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -/** Top level class for exceptions thrown by the driver. */ -public class DriverException extends RuntimeException { - - private static final long serialVersionUID = 0; - - public DriverException(String message) { - super(message); - } - - public DriverException(Throwable cause) { - super(cause); - } - - public DriverException(String message, Throwable cause) { - super(message, cause); - } - - /** - * Copy the exception. - * - *

This returns a new exception, equivalent to the original one, except that because a new - * object is created in the current thread, the top-most element in the stacktrace of the - * exception will refer to the current thread (this is mainly intended for internal use by the - * driver). The cause of the copied exception will be the original exception. - * - * @return a copy/clone of this exception. - */ - public DriverException copy() { - return new DriverException(getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/DriverInternalError.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/DriverInternalError.java deleted file mode 100644 index 68a4c6f9801..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/DriverInternalError.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -/** - * An unexpected error happened internally. - * - *

This should never be raised and indicates a bug (either in the driver or in Cassandra). - */ -public class DriverInternalError extends DriverException { - - private static final long serialVersionUID = 0; - - public DriverInternalError(String message) { - super(message); - } - - public DriverInternalError(Throwable cause) { - super(cause); - } - - public DriverInternalError(String message, Throwable cause) { - super(message, cause); - } - - @Override - public DriverInternalError copy() { - return new DriverInternalError(getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/FrameTooLongException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/FrameTooLongException.java deleted file mode 100644 index af8fae0dd15..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/FrameTooLongException.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -/** - * Indicates that the response frame for a request exceeded {@link - * com.datastax.driver.core.Frame.Decoder.DecoderForStreamIdSize#MAX_FRAME_LENGTH} (default: 256MB, - * configurable via com.datastax.driver.NATIVE_TRANSPORT_MAX_FRAME_SIZE_IN_MB system property) and - * thus was not parsed. - */ -public class FrameTooLongException extends DriverException { - - private static final long serialVersionUID = 0; - - private final int streamId; - - public FrameTooLongException(int streamId) { - this(streamId, null); - } - - private FrameTooLongException(int streamId, Throwable cause) { - super("Response frame exceeded maximum allowed length", cause); - this.streamId = streamId; - } - - /** @return The stream id associated with the frame that caused this exception. */ - public int getStreamId() { - return streamId; - } - - @Override - public FrameTooLongException copy() { - return new FrameTooLongException(streamId, this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/FunctionExecutionException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/FunctionExecutionException.java deleted file mode 100644 index 98e7d01bdfd..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/FunctionExecutionException.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Error during the execution of a function. */ -public class FunctionExecutionException extends QueryExecutionException - implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public FunctionExecutionException(EndPoint endPoint, String msg) { - super(msg); - this.endPoint = endPoint; - } - - private FunctionExecutionException(EndPoint endPoint, String msg, Throwable cause) { - super(msg, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public DriverException copy() { - return new FunctionExecutionException(endPoint, getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/InvalidConfigurationInQueryException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/InvalidConfigurationInQueryException.java deleted file mode 100644 index fa6943e9086..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/InvalidConfigurationInQueryException.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; - -/** - * A specific invalid query exception that indicates that the query is invalid because of some - * configuration problem. - * - *

This is generally throw by query that manipulate the schema (CREATE and ALTER) when the - * required configuration options are invalid. - */ -public class InvalidConfigurationInQueryException extends InvalidQueryException - implements CoordinatorException { - - private static final long serialVersionUID = 0; - - public InvalidConfigurationInQueryException(EndPoint endPoint, String msg) { - super(endPoint, msg); - } - - @Override - public InvalidConfigurationInQueryException copy() { - return new InvalidConfigurationInQueryException(getEndPoint(), getMessage()); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/InvalidQueryException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/InvalidQueryException.java deleted file mode 100644 index 860ea260644..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/InvalidQueryException.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Indicates a syntactically correct but invalid query. */ -public class InvalidQueryException extends QueryValidationException - implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public InvalidQueryException(String msg) { - this(null, msg); - } - - public InvalidQueryException(EndPoint endPoint, String msg) { - super(msg); - this.endPoint = endPoint; - } - - public InvalidQueryException(String msg, Throwable cause) { - this(null, msg, cause); - } - - public InvalidQueryException(EndPoint endPoint, String msg, Throwable cause) { - super(msg, cause); - this.endPoint = endPoint; - } - - @Override - public DriverException copy() { - return new InvalidQueryException(getEndPoint(), getMessage(), this); - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/InvalidTypeException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/InvalidTypeException.java deleted file mode 100644 index 4e3f6baec07..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/InvalidTypeException.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -/** - * Thrown when a {@link com.datastax.driver.core.TypeCodec} is unable to perform the requested - * operation (serialization, deserialization, parsing or formatting) because the object or the byte - * buffer content being processed does not comply with the expected Java and/or CQL type. - */ -public class InvalidTypeException extends DriverException { - - private static final long serialVersionUID = 0; - - public InvalidTypeException(String msg) { - super(msg); - } - - public InvalidTypeException(String msg, Throwable cause) { - super(msg, cause); - } - - @Override - public InvalidTypeException copy() { - return new InvalidTypeException(getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/NoHostAvailableException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/NoHostAvailableException.java deleted file mode 100644 index ce9d7e3cad0..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/NoHostAvailableException.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.HashMap; -import java.util.Map; - -/** - * Exception thrown when a query cannot be performed because no host is available. - * - *

This exception is thrown if either: - * - *

    - *
  • there is no host live in the cluster at the moment of the query; - *
  • all hosts that have been tried have failed. - *
- * - *

For debugging purposes, the list of hosts that have been tried along with the failure cause - * can be retrieved using the {@link #getErrors()} method. - */ -public class NoHostAvailableException extends DriverException { - - private static final long serialVersionUID = 0; - - private static final int MAX_ERRORS_IN_DEFAULT_MESSAGE = 3; - - private final Map errors; - - public NoHostAvailableException(Map errors) { - super(makeMessage(errors, MAX_ERRORS_IN_DEFAULT_MESSAGE, false, false)); - this.errors = errors; - } - - private NoHostAvailableException( - String message, Throwable cause, Map errors) { - super(message, cause); - this.errors = errors; - } - - /** - * Return the hosts tried along with the error encountered while trying them. - * - * @return a map containing for each tried host the error triggered when trying it. - */ - public Map getErrors() { - return new HashMap(errors); - } - - /** - * Builds a custom message for this exception. - * - * @param maxErrors the maximum number of errors displayed (useful to limit the size of the - * message for big clusters). Beyond this limit, host names are still displayed, but not the - * associated errors. Set to {@code Integer.MAX_VALUE} to display all hosts. - * @param formatted whether to format the output (line break between each host). - * @param includeStackTraces whether to include the full stacktrace of each host error. Note that - * this automatically implies {@code formatted}. - * @return the message. - */ - public String getCustomMessage(int maxErrors, boolean formatted, boolean includeStackTraces) { - if (includeStackTraces) formatted = true; - return makeMessage(errors, maxErrors, formatted, includeStackTraces); - } - - @Override - public NoHostAvailableException copy() { - return new NoHostAvailableException(getMessage(), this, errors); - } - - private static String makeMessage( - Map errors, - int maxErrorsInMessage, - boolean formatted, - boolean includeStackTraces) { - if (errors.size() == 0) return "All host(s) tried for query failed (no host was tried)"; - - StringWriter stringWriter = new StringWriter(); - PrintWriter out = new PrintWriter(stringWriter); - - out.print("All host(s) tried for query failed (tried:"); - out.print(formatted ? "\n" : " "); - - int n = 0; - boolean truncated = false; - for (Map.Entry entry : errors.entrySet()) { - if (n > 0) out.print(formatted ? "\n" : ", "); - out.print(entry.getKey()); - if (n < maxErrorsInMessage) { - if (includeStackTraces) { - out.print("\n"); - entry.getValue().printStackTrace(out); - out.print("\n"); - } else { - out.printf(" (%s)", entry.getValue()); - } - } else { - truncated = true; - } - n += 1; - } - if (truncated) { - out.print(formatted ? "\n" : " "); - out.printf( - "[only showing errors of first %d hosts, use getErrors() for more details]", - maxErrorsInMessage); - } - if (formatted && !includeStackTraces) out.print("\n"); - out.print(")"); - out.close(); - return stringWriter.toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/OperationTimedOutException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/OperationTimedOutException.java deleted file mode 100644 index a6c4b670fb3..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/OperationTimedOutException.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import com.datastax.driver.core.SocketOptions; - -/** - * Thrown on a client-side timeout, i.e. when the client didn't hear back from the server within - * {@link SocketOptions#getReadTimeoutMillis()}. - */ -public class OperationTimedOutException extends ConnectionException { - - private static final long serialVersionUID = 0; - - public OperationTimedOutException(EndPoint endPoint) { - super(endPoint, "Operation timed out"); - } - - public OperationTimedOutException(EndPoint endPoint, String msg) { - super(endPoint, msg); - } - - public OperationTimedOutException(EndPoint endPoint, String msg, Throwable cause) { - super(endPoint, msg, cause); - } - - @Override - public OperationTimedOutException copy() { - return new OperationTimedOutException(getEndPoint(), getRawMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/OverloadedException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/OverloadedException.java deleted file mode 100644 index 27efd6c58cc..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/OverloadedException.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Indicates that the contacted host reported itself being overloaded. */ -public class OverloadedException extends QueryExecutionException implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public OverloadedException(EndPoint endPoint, String message) { - super(String.format("Queried host (%s) was overloaded: %s", endPoint, message)); - this.endPoint = endPoint; - } - - /** Private constructor used solely when copying exceptions. */ - private OverloadedException(EndPoint endPoint, String message, OverloadedException cause) { - super(message, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public OverloadedException copy() { - return new OverloadedException(endPoint, getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/PagingStateException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/PagingStateException.java deleted file mode 100644 index 0f1fe9c25f1..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/PagingStateException.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -/** - * Indicates an error while deserializing a (previously serialized) {@link - * com.datastax.driver.core.PagingState} object, or when a paging state does not match the statement - * being executed. - * - * @see com.datastax.driver.core.PagingState - */ -public class PagingStateException extends DriverException { - - private static final long serialVersionUID = 0; - - public PagingStateException(String msg) { - super(msg); - } - - public PagingStateException(String msg, Throwable cause) { - super(msg, cause); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/ProtocolError.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/ProtocolError.java deleted file mode 100644 index 76ab71b1ce3..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/ProtocolError.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** - * Indicates that the contacted host reported a protocol error. Protocol errors indicate that the - * client triggered a protocol violation (for instance, a QUERY message is sent before a STARTUP one - * has been sent). Protocol errors should be considered as a bug in the driver and reported as such. - */ -public class ProtocolError extends DriverInternalError implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public ProtocolError(EndPoint endPoint, String message) { - super( - String.format( - "An unexpected protocol error occurred on host %s. This is a bug in this library, please report: %s", - endPoint, message)); - this.endPoint = endPoint; - } - - /** Private constructor used solely when copying exceptions. */ - private ProtocolError(EndPoint endPoint, String message, ProtocolError cause) { - super(message, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public ProtocolError copy() { - return new ProtocolError(endPoint, getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/QueryConsistencyException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/QueryConsistencyException.java deleted file mode 100644 index 931c519cf4b..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/QueryConsistencyException.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** - * A failure to reach the required consistency level during the execution of a query. - * - *

Such an exception is returned when the query has been tried by Cassandra but cannot be - * achieved with the requested consistency level because either: - * - *

    - *
  • the coordinator did not receive enough replica responses within the rpc timeout set for - * Cassandra; - *
  • some replicas replied with an error. - *
- * - * . - */ -@SuppressWarnings("serial") -public abstract class QueryConsistencyException extends QueryExecutionException - implements CoordinatorException { - - private final EndPoint endPoint; - private final ConsistencyLevel consistency; - private final int received; - private final int required; - - protected QueryConsistencyException( - EndPoint endPoint, String msg, ConsistencyLevel consistency, int received, int required) { - super(msg); - this.endPoint = endPoint; - this.consistency = consistency; - this.received = received; - this.required = required; - } - - protected QueryConsistencyException( - EndPoint endPoint, - String msg, - Throwable cause, - ConsistencyLevel consistency, - int received, - int required) { - super(msg, cause); - this.endPoint = endPoint; - this.consistency = consistency; - this.received = received; - this.required = required; - } - - /** - * The consistency level of the operation that failed. - * - * @return the consistency level of the operation that failed. - */ - public ConsistencyLevel getConsistencyLevel() { - return consistency; - } - - /** - * The number of replicas that had acknowledged/responded to the operation before it failed. - * - * @return the number of replica that had acknowledged/responded the operation before it failed. - */ - public int getReceivedAcknowledgements() { - return received; - } - - /** - * The minimum number of replica acknowledgements/responses that were required to fulfill the - * operation. - * - * @return The minimum number of replica acknowledgements/response that were required to fulfill - * the operation. - */ - public int getRequiredAcknowledgements() { - return required; - } - - /** - * {@inheritDoc} - * - *

Note that this is the information of the host that coordinated the query, not the - * one that timed out. - */ - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/QueryExecutionException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/QueryExecutionException.java deleted file mode 100644 index 7a343c1b4a8..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/QueryExecutionException.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -/** - * Exception related to the execution of a query. - * - *

This corresponds to the exception that Cassandra throws when a (valid) query cannot be - * executed (TimeoutException, UnavailableException, ...). - */ -@SuppressWarnings("serial") -public abstract class QueryExecutionException extends DriverException { - - protected QueryExecutionException(String msg) { - super(msg); - } - - protected QueryExecutionException(String msg, Throwable cause) { - super(msg, cause); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/QueryValidationException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/QueryValidationException.java deleted file mode 100644 index d6c23cf787f..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/QueryValidationException.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -/** - * An exception indicating that a query cannot be executed because it is syntactically incorrect, - * invalid, unauthorized or any other reason. - */ -@SuppressWarnings("serial") -public abstract class QueryValidationException extends DriverException { - - protected QueryValidationException(String msg) { - super(msg); - } - - protected QueryValidationException(String msg, Throwable cause) { - super(msg, cause); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/ReadFailureException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/ReadFailureException.java deleted file mode 100644 index b0e366afe74..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/ReadFailureException.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.util.Collections; -import java.util.Map; - -/** - * A non-timeout error during a read query. - * - *

This happens when some of the replicas that were contacted by the coordinator replied with an - * error. - */ -@SuppressWarnings("serial") -public class ReadFailureException extends QueryConsistencyException { - - private final int failed; - private final boolean dataPresent; - private final Map failuresMap; - - /** - * This constructor should only be used internally by the driver when decoding error responses. - */ - public ReadFailureException( - ConsistencyLevel consistency, - int received, - int required, - int failed, - Map failuresMap, - boolean dataPresent) { - this(null, consistency, received, required, failed, failuresMap, dataPresent); - } - - /** @deprecated Legacy constructor for backward compatibility. */ - @Deprecated - public ReadFailureException( - ConsistencyLevel consistency, int received, int required, int failed, boolean dataPresent) { - this( - null, - consistency, - received, - required, - failed, - Collections.emptyMap(), - dataPresent); - } - - public ReadFailureException( - EndPoint endPoint, - ConsistencyLevel consistency, - int received, - int required, - int failed, - Map failuresMap, - boolean dataPresent) { - super( - endPoint, - String.format( - "Cassandra failure during read query at consistency %s " - + "(%d responses were required but only %d replica responded, %d failed)", - consistency, required, received, failed), - consistency, - received, - required); - this.failed = failed; - this.failuresMap = failuresMap; - this.dataPresent = dataPresent; - } - - /** @deprecated Legacy constructor for backward compatibility. */ - @Deprecated - public ReadFailureException( - EndPoint endPoint, - ConsistencyLevel consistency, - int received, - int required, - int failed, - boolean dataPresent) { - this( - endPoint, - consistency, - received, - required, - failed, - Collections.emptyMap(), - dataPresent); - } - - private ReadFailureException( - EndPoint endPoint, - String msg, - Throwable cause, - ConsistencyLevel consistency, - int received, - int required, - int failed, - Map failuresMap, - boolean dataPresent) { - super(endPoint, msg, cause, consistency, received, required); - this.failed = failed; - this.failuresMap = failuresMap; - this.dataPresent = dataPresent; - } - - /** - * Returns the number of replicas that experienced a failure while executing the request. - * - * @return the number of failures. - */ - public int getFailures() { - return failed; - } - - /** - * Returns the a failure reason code for each node that failed. - * - *

At the time of writing, the existing reason codes are: - * - *

    - *
  • {@code 0x0000}: the error does not have a specific code assigned yet, or the cause is - * unknown. - *
  • {@code 0x0001}: The read operation scanned too many tombstones (as defined by {@code - * tombstone_failure_threshold} in {@code cassandra.yaml}, causing a {@code - * TombstoneOverwhelmingException}. - *
- * - * (please refer to the Cassandra documentation for your version for the most up-to-date list of - * errors) - * - *

This feature is available for protocol v5 or above only. With lower protocol versions, the - * map will always be empty. - * - * @return a map of IP addresses to failure codes. - */ - public Map getFailuresMap() { - return failuresMap; - } - - /** - * Whether the actual data was amongst the received replica responses. - * - *

During reads, Cassandra doesn't request data from every replica to minimize internal network - * traffic. Instead, some replicas are only asked for a checksum of the data. A read timeout may - * occurred even if enough replicas have responded to fulfill the consistency level if only - * checksum responses have been received. This method allows to detect that case. - * - * @return whether the data was amongst the received replica responses. - */ - public boolean wasDataRetrieved() { - return dataPresent; - } - - @Override - public ReadFailureException copy() { - return new ReadFailureException( - getEndPoint(), - getMessage(), - this, - getConsistencyLevel(), - getReceivedAcknowledgements(), - getRequiredAcknowledgements(), - getFailures(), - getFailuresMap(), - wasDataRetrieved()); - } - - public ReadFailureException copy(EndPoint endPoint) { - return new ReadFailureException( - endPoint, - getMessage(), - this, - getConsistencyLevel(), - getReceivedAcknowledgements(), - getRequiredAcknowledgements(), - failed, - getFailuresMap(), - dataPresent); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/ReadTimeoutException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/ReadTimeoutException.java deleted file mode 100644 index 71a266fdd19..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/ReadTimeoutException.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.EndPoint; - -/** A Cassandra timeout during a read query. */ -public class ReadTimeoutException extends QueryConsistencyException { - - private static final long serialVersionUID = 0; - - private final boolean dataPresent; - - /** - * This constructor should only be used internally by the driver when decoding error responses. - */ - public ReadTimeoutException( - ConsistencyLevel consistency, int received, int required, boolean dataPresent) { - this(null, consistency, received, required, dataPresent); - } - - public ReadTimeoutException( - EndPoint endPoint, - ConsistencyLevel consistency, - int received, - int required, - boolean dataPresent) { - super( - endPoint, - String.format( - "Cassandra timeout during read query at consistency %s (%s). " - + "In case this was generated during read repair, the consistency level is not representative of the actual consistency.", - consistency, formatDetails(received, required, dataPresent)), - consistency, - received, - required); - this.dataPresent = dataPresent; - } - - private ReadTimeoutException( - EndPoint endPoint, - String msg, - Throwable cause, - ConsistencyLevel consistency, - int received, - int required, - boolean dataPresent) { - super(endPoint, msg, cause, consistency, received, required); - this.dataPresent = dataPresent; - } - - private static String formatDetails(int received, int required, boolean dataPresent) { - if (received < required) - return String.format( - "%d responses were required but only %d replica responded", required, received); - else if (!dataPresent) return "the replica queried for data didn't respond"; - else return "timeout while waiting for repair of inconsistent replica"; - } - - /** - * Whether the actual data was amongst the received replica responses. - * - *

During reads, Cassandra doesn't request data from every replica to minimize internal network - * traffic. Instead, some replicas are only asked for a checksum of the data. A read timeout may - * have occurred even if enough replicas have responded to fulfill the consistency level, if only - * checksum responses have been received. This method allows to detect that case. - * - * @return whether the data was amongst the received replica responses. - */ - public boolean wasDataRetrieved() { - return dataPresent; - } - - @Override - public ReadTimeoutException copy() { - return new ReadTimeoutException( - getEndPoint(), - getMessage(), - this, - getConsistencyLevel(), - getReceivedAcknowledgements(), - getRequiredAcknowledgements(), - wasDataRetrieved()); - } - - /** - * Create a copy of this exception with a nicer stack trace, and including the coordinator address - * that caused this exception to be raised. - * - *

This method is mainly intended for internal use by the driver and exists mainly because: - * - *

    - *
  1. the original exception was decoded from a response frame and at that time, the - * coordinator address was not available; and - *
  2. the newly-created exception will refer to the current thread in its stack trace, which - * generally yields a more user-friendly stack trace that the original one. - *
- * - * @param endPoint The full address of the host that caused this exception to be thrown. - * @return a copy/clone of this exception, but with the given host address instead of the original - * one. - */ - public ReadTimeoutException copy(EndPoint endPoint) { - return new ReadTimeoutException( - endPoint, - getMessage(), - this, - getConsistencyLevel(), - getReceivedAcknowledgements(), - getRequiredAcknowledgements(), - wasDataRetrieved()); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/ServerError.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/ServerError.java deleted file mode 100644 index c96e77a7591..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/ServerError.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** - * Indicates that the contacted host reported an internal error. This should be considered as a bug - * in Cassandra and reported as such. - */ -public class ServerError extends DriverInternalError implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public ServerError(EndPoint endPoint, String message) { - super(String.format("An unexpected error occurred server side on %s: %s", endPoint, message)); - this.endPoint = endPoint; - } - - /** Private constructor used solely when copying exceptions. */ - private ServerError(EndPoint endPoint, String message, ServerError cause) { - super(message, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public ServerError copy() { - return new ServerError(endPoint, getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/SyntaxError.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/SyntaxError.java deleted file mode 100644 index 11da48a3dcf..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/SyntaxError.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Indicates a syntax error in a query. */ -public class SyntaxError extends QueryValidationException implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public SyntaxError(EndPoint endPoint, String msg) { - super(msg); - this.endPoint = endPoint; - } - - private SyntaxError(EndPoint endPoint, String msg, Throwable cause) { - super(msg, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public SyntaxError copy() { - return new SyntaxError(getEndPoint(), getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/TraceRetrievalException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/TraceRetrievalException.java deleted file mode 100644 index 21295474663..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/TraceRetrievalException.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -/** - * Exception thrown if a query trace cannot be retrieved. - * - * @see com.datastax.driver.core.QueryTrace - */ -public class TraceRetrievalException extends DriverException { - - private static final long serialVersionUID = 0; - - public TraceRetrievalException(String message) { - super(message); - } - - public TraceRetrievalException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public TraceRetrievalException copy() { - return new TraceRetrievalException(getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/TransportException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/TransportException.java deleted file mode 100644 index 6134bbc178b..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/TransportException.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; - -/** - * A connection exception that has to do with the transport itself, i.e. that suggests the node is - * down. - */ -public class TransportException extends ConnectionException { - - private static final long serialVersionUID = 0; - - public TransportException(EndPoint endPoint, String msg, Throwable cause) { - super(endPoint, msg, cause); - } - - public TransportException(EndPoint endPoint, String msg) { - super(endPoint, msg); - } - - @Override - public TransportException copy() { - return new TransportException(getEndPoint(), getRawMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/TruncateException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/TruncateException.java deleted file mode 100644 index c62627f4c99..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/TruncateException.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Error during a truncation operation. */ -public class TruncateException extends QueryExecutionException implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public TruncateException(EndPoint endPoint, String msg) { - super(msg); - this.endPoint = endPoint; - } - - private TruncateException(EndPoint endPoint, String msg, Throwable cause) { - super(msg, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public TruncateException copy() { - return new TruncateException(getEndPoint(), getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnauthorizedException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnauthorizedException.java deleted file mode 100644 index 477756096ad..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnauthorizedException.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** - * Indicates that a query cannot be performed due to the authorization restrictions of the logged - * user. - */ -public class UnauthorizedException extends QueryValidationException - implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public UnauthorizedException(EndPoint endPoint, String msg) { - super(msg); - this.endPoint = endPoint; - } - - private UnauthorizedException(EndPoint endPoint, String msg, Throwable cause) { - super(msg, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public UnauthorizedException copy() { - return new UnauthorizedException(getEndPoint(), getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnavailableException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnavailableException.java deleted file mode 100644 index 15f576f44c2..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnavailableException.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** - * Exception thrown when the coordinator knows there is not enough replicas alive to perform a query - * with the requested consistency level. - */ -public class UnavailableException extends QueryExecutionException implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - private final ConsistencyLevel consistency; - private final int required; - private final int alive; - - /** - * This constructor should only be used internally by the driver when decoding error responses. - */ - public UnavailableException(ConsistencyLevel consistency, int required, int alive) { - this(null, consistency, required, alive); - } - - public UnavailableException( - EndPoint endPoint, ConsistencyLevel consistency, int required, int alive) { - super( - String.format( - "Not enough replicas available for query at consistency %s (%d required but only %d alive)", - consistency, required, alive)); - this.endPoint = endPoint; - this.consistency = consistency; - this.required = required; - this.alive = alive; - } - - private UnavailableException( - EndPoint endPoint, - String message, - Throwable cause, - ConsistencyLevel consistency, - int required, - int alive) { - super(message, cause); - this.endPoint = endPoint; - this.consistency = consistency; - this.required = required; - this.alive = alive; - } - - /** - * The consistency level of the operation triggering this unavailable exception. - * - * @return the consistency level of the operation triggering this unavailable exception. - */ - public ConsistencyLevel getConsistencyLevel() { - return consistency; - } - - /** - * The number of replica acknowledgements/responses required to perform the operation (with its - * required consistency level). - * - * @return the number of replica acknowledgements/responses required to perform the operation. - */ - public int getRequiredReplicas() { - return required; - } - - /** - * The number of replicas that were known to be alive by the Cassandra coordinator node when it - * tried to execute the operation. - * - * @return The number of replicas that were known to be alive by the Cassandra coordinator node - * when it tried to execute the operation. - */ - public int getAliveReplicas() { - return alive; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public UnavailableException copy() { - return new UnavailableException( - getEndPoint(), getMessage(), this, consistency, required, alive); - } - - /** - * Create a copy of this exception with a nicer stack trace, and including the coordinator address - * that caused this exception to be raised. - * - *

This method is mainly intended for internal use by the driver and exists mainly because: - * - *

    - *
  1. the original exception was decoded from a response frame and at that time, the - * coordinator address was not available; and - *
  2. the newly-created exception will refer to the current thread in its stack trace, which - * generally yields a more user-friendly stack trace that the original one. - *
- * - * @param endPoint The full connection information of the host that caused this exception to be - * thrown. - * @return a copy/clone of this exception, but with the given host address instead of the original - * one. - */ - public UnavailableException copy(EndPoint endPoint) { - return new UnavailableException(endPoint, getMessage(), this, consistency, required, alive); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnpreparedException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnpreparedException.java deleted file mode 100644 index 8f5b7618573..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnpreparedException.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** Indicates that the contacted host replied with an UNPREPARED error code. */ -public class UnpreparedException extends QueryValidationException implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - public UnpreparedException(EndPoint endPoint, String message) { - super( - String.format( - "A prepared query was submitted on %s but was not known of that node: %s", - endPoint, message)); - this.endPoint = endPoint; - } - - /** Private constructor used solely when copying exceptions. */ - private UnpreparedException(EndPoint endPoint, String message, UnpreparedException cause) { - super(message, cause); - this.endPoint = endPoint; - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - @Override - public UnpreparedException copy() { - return new UnpreparedException(endPoint, getMessage(), this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnresolvedUserTypeException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnresolvedUserTypeException.java deleted file mode 100644 index c42a0d81dcd..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnresolvedUserTypeException.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -/** - * Thrown when a user type cannot be resolved. - * - *

This exception can be raised when the driver is rebuilding its schema metadata, and a - * user-defined type cannot be completely constructed due to some missing information. It should - * only appear in the driver logs, never in client code. It shouldn't be considered as a severe - * error as long as it only appears occasionally. - */ -public class UnresolvedUserTypeException extends DriverException { - - private final String keyspaceName; - - private final String name; - - public UnresolvedUserTypeException(String keyspaceName, String name) { - super(String.format("Cannot resolve user type %s.%s", keyspaceName, name)); - this.keyspaceName = keyspaceName; - this.name = name; - } - - private UnresolvedUserTypeException(String keyspaceName, String name, Throwable cause) { - super(String.format("Cannot resolve user type %s.%s", keyspaceName, name), cause); - this.keyspaceName = keyspaceName; - this.name = name; - } - - public String getKeyspaceName() { - return keyspaceName; - } - - public String getName() { - return name; - } - - @Override - public UnresolvedUserTypeException copy() { - return new UnresolvedUserTypeException(keyspaceName, name, this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnsupportedFeatureException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnsupportedFeatureException.java deleted file mode 100644 index 7d4e4d6613b..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnsupportedFeatureException.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.ProtocolVersion; - -/** Exception thrown when a feature is not supported by the native protocol currently in use. */ -public class UnsupportedFeatureException extends DriverException { - - private static final long serialVersionUID = 0; - - private final ProtocolVersion currentVersion; - - public UnsupportedFeatureException(ProtocolVersion currentVersion, String msg) { - super( - "Unsupported feature with the native protocol " - + currentVersion - + " (which is currently in use): " - + msg); - this.currentVersion = currentVersion; - } - - public ProtocolVersion getCurrentVersion() { - return currentVersion; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnsupportedProtocolVersionException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnsupportedProtocolVersionException.java deleted file mode 100644 index be52d5a52a5..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/UnsupportedProtocolVersionException.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import com.datastax.driver.core.ProtocolVersion; -import java.net.InetAddress; -import java.net.InetSocketAddress; - -/** - * Indicates that we've attempted to connect to a Cassandra node with a protocol version that it - * cannot handle (e.g., connecting to a C* 1.2 node with protocol version 2). - */ -public class UnsupportedProtocolVersionException extends DriverException - implements CoordinatorException { - - private static final long serialVersionUID = 0; - - private final EndPoint endPoint; - - private final ProtocolVersion unsupportedVersion; - - private final ProtocolVersion serverVersion; - - public UnsupportedProtocolVersionException( - EndPoint endPoint, ProtocolVersion unsupportedVersion, ProtocolVersion serverVersion) { - super(makeErrorMessage(endPoint, unsupportedVersion, serverVersion)); - this.endPoint = endPoint; - this.unsupportedVersion = unsupportedVersion; - this.serverVersion = serverVersion; - } - - public UnsupportedProtocolVersionException( - EndPoint endPoint, - ProtocolVersion unsupportedVersion, - ProtocolVersion serverVersion, - Throwable cause) { - super(makeErrorMessage(endPoint, unsupportedVersion, serverVersion), cause); - this.endPoint = endPoint; - this.unsupportedVersion = unsupportedVersion; - this.serverVersion = serverVersion; - } - - private static String makeErrorMessage( - EndPoint endPoint, ProtocolVersion unsupportedVersion, ProtocolVersion serverVersion) { - return unsupportedVersion == serverVersion - ? String.format( - "[%s] Host does not support protocol version %s", endPoint, unsupportedVersion) - : String.format( - "[%s] Host does not support protocol version %s but %s", - endPoint, unsupportedVersion, serverVersion); - } - - @Override - public EndPoint getEndPoint() { - return endPoint; - } - - @Override - @Deprecated - public InetSocketAddress getAddress() { - return (endPoint == null) ? null : endPoint.resolve(); - } - - @Override - @Deprecated - public InetAddress getHost() { - return (endPoint == null) ? null : endPoint.resolve().getAddress(); - } - - /** - * The version with which the server replied. - * - *

Note that this version is not necessarily a supported version. While this is usually the - * case, in rare situations, the server might respond with an unsupported version, to ensure that - * the client can decode its response properly. See CASSANDRA-11464 for more details. - * - * @return The version with which the server replied. - */ - public ProtocolVersion getServerVersion() { - return serverVersion; - } - - /** - * The version with which the client sent its request. - * - * @return The version with which the client sent its request. - */ - public ProtocolVersion getUnsupportedVersion() { - return unsupportedVersion; - } - - @Override - public UnsupportedProtocolVersionException copy() { - return new UnsupportedProtocolVersionException( - endPoint, unsupportedVersion, serverVersion, this); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/WrappingEndPoint.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/WrappingEndPoint.java deleted file mode 100644 index f97b6ae36c5..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/WrappingEndPoint.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.EndPoint; -import java.net.InetSocketAddress; - -// The sole purpose of this class is to allow some exception types to preserve a constructor that -// takes an InetSocketAddress (for backward compatibility). -class WrappingEndPoint implements EndPoint { - private final InetSocketAddress address; - - WrappingEndPoint(InetSocketAddress address) { - this.address = address; - } - - @Override - public InetSocketAddress resolve() { - return address; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/WriteFailureException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/WriteFailureException.java deleted file mode 100644 index 76ef37244af..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/WriteFailureException.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.EndPoint; -import com.datastax.driver.core.WriteType; -import java.net.InetAddress; -import java.util.Collections; -import java.util.Map; - -/** - * A non-timeout error during a write query. - * - *

This happens when some of the replicas that were contacted by the coordinator replied with an - * error. - */ -@SuppressWarnings("serial") -public class WriteFailureException extends QueryConsistencyException { - private final WriteType writeType; - private final int failed; - private final Map failuresMap; - - /** - * This constructor should only be used internally by the driver when decoding error responses. - */ - public WriteFailureException( - ConsistencyLevel consistency, - WriteType writeType, - int received, - int required, - int failed, - Map failuresMap) { - this(null, consistency, writeType, received, required, failed, failuresMap); - } - - /** @deprecated Legacy constructor for backward compatibility. */ - @Deprecated - public WriteFailureException( - ConsistencyLevel consistency, WriteType writeType, int received, int required, int failed) { - this( - null, - consistency, - writeType, - received, - required, - failed, - Collections.emptyMap()); - } - - public WriteFailureException( - EndPoint endPoint, - ConsistencyLevel consistency, - WriteType writeType, - int received, - int required, - int failed, - Map failuresMap) { - super( - endPoint, - String.format( - "Cassandra failure during write query at consistency %s " - + "(%d responses were required but only %d replica responded, %d failed)", - consistency, required, received, failed), - consistency, - received, - required); - this.writeType = writeType; - this.failed = failed; - this.failuresMap = failuresMap; - } - - /** @deprecated Legacy constructor for backward compatibility. */ - @Deprecated - public WriteFailureException( - EndPoint endPoint, - ConsistencyLevel consistency, - WriteType writeType, - int received, - int required, - int failed) { - this( - endPoint, - consistency, - writeType, - received, - required, - failed, - Collections.emptyMap()); - } - - private WriteFailureException( - EndPoint endPoint, - String msg, - Throwable cause, - ConsistencyLevel consistency, - WriteType writeType, - int received, - int required, - int failed, - Map failuresMap) { - super(endPoint, msg, cause, consistency, received, required); - this.writeType = writeType; - this.failed = failed; - this.failuresMap = failuresMap; - } - - /** - * The type of the write for which a timeout was raised. - * - * @return the type of the write for which a timeout was raised. - */ - public WriteType getWriteType() { - return writeType; - } - - /** - * Returns the number of replicas that experienced a failure while executing the request. - * - * @return the number of failures. - */ - public int getFailures() { - return failed; - } - - /** - * Returns the a failure reason code for each node that failed. - * - *

At the time of writing, the existing reason codes are: - * - *

    - *
  • {@code 0x0000}: the error does not have a specific code assigned yet, or the cause is - * unknown. - *
  • {@code 0x0001}: The read operation scanned too many tombstones (as defined by {@code - * tombstone_failure_threshold} in {@code cassandra.yaml}, causing a {@code - * TombstoneOverwhelmingException}. - *
- * - * (please refer to the Cassandra documentation for your version for the most up-to-date list of - * errors) - * - *

This feature is available for protocol v5 or above only. With lower protocol versions, the - * map will always be empty. - * - * @return a map of IP addresses to failure codes. - */ - public Map getFailuresMap() { - return failuresMap; - } - - @Override - public WriteFailureException copy() { - return new WriteFailureException( - getEndPoint(), - getMessage(), - this, - getConsistencyLevel(), - getWriteType(), - getReceivedAcknowledgements(), - getRequiredAcknowledgements(), - getFailures(), - failuresMap); - } - - public WriteFailureException copy(EndPoint endPoint) { - return new WriteFailureException( - endPoint, - getMessage(), - this, - getConsistencyLevel(), - getWriteType(), - getReceivedAcknowledgements(), - getRequiredAcknowledgements(), - failed, - failuresMap); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/WriteTimeoutException.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/WriteTimeoutException.java deleted file mode 100644 index 52377fd4159..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/WriteTimeoutException.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.exceptions; - -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.EndPoint; -import com.datastax.driver.core.WriteType; - -/** A Cassandra timeout during a write query. */ -public class WriteTimeoutException extends QueryConsistencyException { - - private static final long serialVersionUID = 0; - - private final WriteType writeType; - - /** - * This constructor should only be used internally by the driver when decoding error responses. - */ - public WriteTimeoutException( - ConsistencyLevel consistency, WriteType writeType, int received, int required) { - this(null, consistency, writeType, received, required); - } - - public WriteTimeoutException( - EndPoint endPoint, - ConsistencyLevel consistency, - WriteType writeType, - int received, - int required) { - super( - endPoint, - String.format( - "Cassandra timeout during %s write query at consistency %s " - + "(%d replica were required but only %d acknowledged the write)", - writeType, consistency, required, received), - consistency, - received, - required); - this.writeType = writeType; - } - - private WriteTimeoutException( - EndPoint endPoint, - String msg, - Throwable cause, - ConsistencyLevel consistency, - WriteType writeType, - int received, - int required) { - super(endPoint, msg, cause, consistency, received, required); - this.writeType = writeType; - } - - /** - * The type of the write for which a timeout was raised. - * - * @return the type of the write for which a timeout was raised. - */ - public WriteType getWriteType() { - return writeType; - } - - @Override - public WriteTimeoutException copy() { - return new WriteTimeoutException( - getEndPoint(), - getMessage(), - this, - getConsistencyLevel(), - getWriteType(), - getReceivedAcknowledgements(), - getRequiredAcknowledgements()); - } - - /** - * Create a copy of this exception with a nicer stack trace, and including the coordinator address - * that caused this exception to be raised. - * - *

This method is mainly intended for internal use by the driver and exists mainly because: - * - *

    - *
  1. the original exception was decoded from a response frame and at that time, the - * coordinator address was not available; and - *
  2. the newly-created exception will refer to the current thread in its stack trace, which - * generally yields a more user-friendly stack trace that the original one. - *
- * - * @param address The full address of the host that caused this exception to be thrown. - * @return a copy/clone of this exception, but with the given host address instead of the original - * one. - */ - public WriteTimeoutException copy(EndPoint address) { - return new WriteTimeoutException( - address, - getMessage(), - this, - getConsistencyLevel(), - getWriteType(), - getReceivedAcknowledgements(), - getRequiredAcknowledgements()); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/exceptions/package-info.java b/driver-core/src/main/java/com/datastax/driver/core/exceptions/package-info.java deleted file mode 100644 index e3980a58b57..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/exceptions/package-info.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** Exceptions thrown by the DataStax Java driver for Cassandra. */ -package com.datastax.driver.core.exceptions; diff --git a/driver-core/src/main/java/com/datastax/driver/core/package-info.java b/driver-core/src/main/java/com/datastax/driver/core/package-info.java deleted file mode 100644 index 995ba870c77..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/package-info.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * The main package for the DataStax Java driver for Cassandra. - * - *

The main entry for this package is the {@link com.datastax.driver.core.Cluster} class. - */ -package com.datastax.driver.core; diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/AddressTranslator.java b/driver-core/src/main/java/com/datastax/driver/core/policies/AddressTranslator.java deleted file mode 100644 index 87bb59364f4..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/AddressTranslator.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import java.net.InetSocketAddress; - -/** - * Translates IP addresses received from Cassandra nodes into locally queriable addresses. - * - *

The driver auto-detect new Cassandra nodes added to the cluster through server side pushed - * notifications and through checking the system tables. For each node, the address the driver will - * receive will correspond to the address set as {@code rpc_address} in the node yaml file. In most - * case, this is the correct address to use by the driver and that is what is used by default. - * However, sometimes the addresses received through this mechanism will either not be reachable - * directly by the driver or should not be the preferred address to use to reach the node (for - * instance, the {@code rpc_address} set on Cassandra nodes might be a private IP, but some clients - * may have to use a public IP, or pass by a router to reach that node). This interface allows to - * deal with such cases, by allowing to translate an address as sent by a Cassandra node to another - * address to be used by the driver for connection. - * - *

Please note that the contact points addresses provided while creating the {@link Cluster} - * instance are not "translated", only IP address retrieved from or sent by Cassandra nodes to the - * driver are. - */ -public interface AddressTranslator { - - /** - * Initializes this address translator. - * - * @param cluster the {@code Cluster} instance for which the translator is created. - */ - void init(Cluster cluster); - - /** - * Translates a Cassandra {@code rpc_address} to another address if necessary. - * - * @param address the address of a node as returned by Cassandra. Note that if the {@code - * rpc_address} of a node has been configured to {@code 0.0.0.0} server side, then the - * provided address will be the node {@code listen_address}, *not* {@code 0.0.0.0}. Also note - * that the port for {@code InetSocketAddress} will always be the one set at Cluster - * construction time (9042 by default). - * @return the address the driver should actually use to connect to the node designated by {@code - * address}. If the return is {@code null}, then {@code address} will be used by the driver - * (it is thus equivalent to returning {@code address} directly) - */ - InetSocketAddress translate(InetSocketAddress address); - - /** Called at {@link Cluster} shutdown. */ - void close(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/ChainableLoadBalancingPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/ChainableLoadBalancingPolicy.java deleted file mode 100644 index 873606ef470..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/ChainableLoadBalancingPolicy.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -/** A load balancing policy that wraps another policy. */ -public interface ChainableLoadBalancingPolicy extends LoadBalancingPolicy { - /** - * Returns the child policy. - * - * @return the child policy. - */ - LoadBalancingPolicy getChildPolicy(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/Clock.java b/driver-core/src/main/java/com/datastax/driver/core/policies/Clock.java deleted file mode 100644 index a10d37c39ca..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/Clock.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -/** Wraps System.nanoTime() to make it easy to mock in tests. */ -class Clock { - static final Clock DEFAULT = new Clock(); - - long nanoTime() { - return System.nanoTime(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/ConstantReconnectionPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/ConstantReconnectionPolicy.java deleted file mode 100644 index d6c782d0c0c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/ConstantReconnectionPolicy.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; - -/** A reconnection policy that waits a constant time between each reconnection attempt. */ -public class ConstantReconnectionPolicy implements ReconnectionPolicy { - - private final long delayMs; - - /** - * Creates a reconnection policy that creates with the provided constant wait time between - * reconnection attempts. - * - * @param constantDelayMs the constant delay in milliseconds to use. - */ - public ConstantReconnectionPolicy(long constantDelayMs) { - if (constantDelayMs < 0) - throw new IllegalArgumentException( - String.format("Invalid negative delay (got %d)", constantDelayMs)); - - this.delayMs = constantDelayMs; - } - - /** - * The constant delay used by this reconnection policy. - * - * @return the constant delay used by this reconnection policy. - */ - public long getConstantDelayMs() { - return delayMs; - } - - /** - * A new schedule that uses a constant {@code getConstantDelayMs()} delay between reconnection - * attempt. - * - * @return the newly created schedule. - */ - @Override - public ReconnectionSchedule newSchedule() { - return new ConstantSchedule(); - } - - private class ConstantSchedule implements ReconnectionSchedule { - - @Override - public long nextDelayMs() { - return delayMs; - } - } - - @Override - public void init(Cluster cluster) { - // nothing to do - } - - @Override - public void close() { - // nothing to do - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/ConstantSpeculativeExecutionPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/ConstantSpeculativeExecutionPolicy.java deleted file mode 100644 index 93e70a197fc..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/ConstantSpeculativeExecutionPolicy.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.Statement; -import com.google.common.base.Preconditions; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * A {@link SpeculativeExecutionPolicy} that schedules a given number of speculative executions, - * separated by a fixed delay. - */ -public class ConstantSpeculativeExecutionPolicy implements SpeculativeExecutionPolicy { - private final int maxSpeculativeExecutions; - private final long constantDelayMillis; - - /** - * Builds a new instance. - * - * @param constantDelayMillis the delay between each speculative execution. Must be >= 0. A zero - * delay means it should immediately send `maxSpeculativeExecutions` requests along with the - * original request. - * @param maxSpeculativeExecutions the number of speculative executions. Must be strictly - * positive. - * @throws IllegalArgumentException if one of the arguments does not respect the preconditions - * above. - */ - public ConstantSpeculativeExecutionPolicy( - final long constantDelayMillis, final int maxSpeculativeExecutions) { - Preconditions.checkArgument( - constantDelayMillis >= 0, "delay must be >= 0 (was %d)", constantDelayMillis); - Preconditions.checkArgument( - maxSpeculativeExecutions > 0, - "number of speculative executions must be strictly positive (was %d)", - maxSpeculativeExecutions); - this.constantDelayMillis = constantDelayMillis; - this.maxSpeculativeExecutions = maxSpeculativeExecutions; - } - - @Override - public SpeculativeExecutionPlan newPlan(String loggedKeyspace, Statement statement) { - return new SpeculativeExecutionPlan() { - private final AtomicInteger remaining = new AtomicInteger(maxSpeculativeExecutions); - - @Override - public long nextExecution(Host lastQueried) { - return (remaining.getAndDecrement() > 0) ? constantDelayMillis : -1; - } - }; - } - - @Override - public void init(Cluster cluster) { - // do nothing - } - - @Override - public void close() { - // do nothing - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/DCAwareRoundRobinPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/DCAwareRoundRobinPolicy.java deleted file mode 100644 index a1274f6b458..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/DCAwareRoundRobinPolicy.java +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Configuration; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.HostDistance; -import com.datastax.driver.core.Statement; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Joiner; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; -import com.google.common.collect.AbstractIterator; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A data-center aware Round-robin load balancing policy. - * - *

This policy provides round-robin queries over the node of the local data center. It also - * includes in the query plans returned a configurable number of hosts in the remote data centers, - * but those are always tried after the local nodes. In other words, this policy guarantees that no - * host in a remote data center will be queried unless no host in the local data center can be - * reached. - * - *

If used with a single data center, this policy is equivalent to the {@link RoundRobinPolicy}, - * but its DC awareness incurs a slight overhead so the latter should be preferred to this policy in - * that case. - */ -public class DCAwareRoundRobinPolicy implements LoadBalancingPolicy { - - private static final Logger logger = LoggerFactory.getLogger(DCAwareRoundRobinPolicy.class); - - /** - * Returns a builder to create a new instance. - * - * @return the builder. - */ - public static Builder builder() { - return new Builder(); - } - - private static final String UNSET = ""; - - private final ConcurrentMap> perDcLiveHosts = - new ConcurrentHashMap>(); - private final AtomicInteger index = new AtomicInteger(); - - @VisibleForTesting volatile String localDc; - - private final int usedHostsPerRemoteDc; - private final boolean dontHopForLocalCL; - - private volatile Configuration configuration; - - private DCAwareRoundRobinPolicy( - String localDc, - int usedHostsPerRemoteDc, - boolean allowRemoteDCsForLocalConsistencyLevel, - boolean allowEmptyLocalDc) { - if (!allowEmptyLocalDc && Strings.isNullOrEmpty(localDc)) - throw new IllegalArgumentException("Null or empty data center specified for DC-aware policy"); - this.localDc = localDc == null ? UNSET : localDc; - this.usedHostsPerRemoteDc = usedHostsPerRemoteDc; - this.dontHopForLocalCL = !allowRemoteDCsForLocalConsistencyLevel; - } - - @Override - public void init(Cluster cluster, Collection hosts) { - if (localDc != UNSET) - logger.info("Using provided data-center name '{}' for DCAwareRoundRobinPolicy", localDc); - - this.configuration = cluster.getConfiguration(); - - ArrayList notInLocalDC = new ArrayList(); - - for (Host host : hosts) { - String dc = dc(host); - - // If the localDC was in "auto-discover" mode and it's the first host for which we have a DC, - // use it. - if (localDc == UNSET && dc != UNSET) { - logger.info( - "Using data-center name '{}' for DCAwareRoundRobinPolicy (if this is incorrect, please provide the correct datacenter name with DCAwareRoundRobinPolicy constructor)", - dc); - localDc = dc; - } else if (!dc.equals(localDc)) - notInLocalDC.add(String.format("%s (%s)", host.toString(), dc)); - - CopyOnWriteArrayList prev = perDcLiveHosts.get(dc); - if (prev == null) - perDcLiveHosts.put(dc, new CopyOnWriteArrayList(Collections.singletonList(host))); - else prev.addIfAbsent(host); - } - - if (notInLocalDC.size() > 0) { - String nonLocalHosts = Joiner.on(",").join(notInLocalDC); - logger.warn( - "Some contact points don't match local data center. Local DC = {}. Non-conforming contact points: {}", - localDc, - nonLocalHosts); - } - - this.index.set(new Random().nextInt(Math.max(hosts.size(), 1))); - } - - private String dc(Host host) { - String dc = host.getDatacenter(); - return dc == null ? localDc : dc; - } - - @SuppressWarnings("unchecked") - private static CopyOnWriteArrayList cloneList(CopyOnWriteArrayList list) { - return (CopyOnWriteArrayList) list.clone(); - } - - /** - * Return the HostDistance for the provided host. - * - *

This policy consider nodes in the local datacenter as {@code LOCAL}. For each remote - * datacenter, it considers a configurable number of hosts as {@code REMOTE} and the rest is - * {@code IGNORED}. - * - *

To configure how many hosts in each remote datacenter should be considered, see {@link - * Builder#withUsedHostsPerRemoteDc(int)}. - * - * @param host the host of which to return the distance of. - * @return the HostDistance to {@code host}. - */ - @Override - public HostDistance distance(Host host) { - String dc = dc(host); - if (dc == UNSET || dc.equals(localDc)) return HostDistance.LOCAL; - - CopyOnWriteArrayList dcHosts = perDcLiveHosts.get(dc); - if (dcHosts == null || usedHostsPerRemoteDc == 0) return HostDistance.IGNORED; - - // We need to clone, otherwise our subList call is not thread safe - dcHosts = cloneList(dcHosts); - return dcHosts.subList(0, Math.min(dcHosts.size(), usedHostsPerRemoteDc)).contains(host) - ? HostDistance.REMOTE - : HostDistance.IGNORED; - } - - /** - * Returns the hosts to use for a new query. - * - *

The returned plan will always try each known host in the local datacenter first, and then, - * if none of the local host is reachable, will try up to a configurable number of other host per - * remote datacenter. The order of the local node in the returned query plan will follow a - * Round-robin algorithm. - * - * @param loggedKeyspace the keyspace currently logged in on for this query. - * @param statement the query for which to build the plan. - * @return a new query plan, i.e. an iterator indicating which host to try first for querying, - * which one to use as failover, etc... - */ - @Override - public Iterator newQueryPlan(String loggedKeyspace, final Statement statement) { - - CopyOnWriteArrayList localLiveHosts = perDcLiveHosts.get(localDc); - final List hosts = - localLiveHosts == null ? Collections.emptyList() : cloneList(localLiveHosts); - final int startIdx = index.getAndIncrement(); - - return new AbstractIterator() { - - private int idx = startIdx; - private int remainingLocal = hosts.size(); - - // For remote Dcs - private Iterator remoteDcs; - private List currentDcHosts; - private int currentDcRemaining; - - @Override - protected Host computeNext() { - while (true) { - if (remainingLocal > 0) { - remainingLocal--; - int c = idx++ % hosts.size(); - if (c < 0) { - c += hosts.size(); - } - return hosts.get(c); - } - - if (currentDcHosts != null && currentDcRemaining > 0) { - currentDcRemaining--; - int c = idx++ % currentDcHosts.size(); - if (c < 0) { - c += currentDcHosts.size(); - } - return currentDcHosts.get(c); - } - - ConsistencyLevel cl = - statement.getConsistencyLevel() == null - ? configuration.getQueryOptions().getConsistencyLevel() - : statement.getConsistencyLevel(); - - if (dontHopForLocalCL && cl.isDCLocal()) return endOfData(); - - if (remoteDcs == null) { - Set copy = new HashSet(perDcLiveHosts.keySet()); - copy.remove(localDc); - remoteDcs = copy.iterator(); - } - - if (!remoteDcs.hasNext()) break; - - String nextRemoteDc = remoteDcs.next(); - CopyOnWriteArrayList nextDcHosts = perDcLiveHosts.get(nextRemoteDc); - if (nextDcHosts != null) { - // Clone for thread safety - List dcHosts = cloneList(nextDcHosts); - currentDcHosts = dcHosts.subList(0, Math.min(dcHosts.size(), usedHostsPerRemoteDc)); - currentDcRemaining = currentDcHosts.size(); - } - } - return endOfData(); - } - }; - } - - @Override - public void onUp(Host host) { - String dc = dc(host); - - // If the localDC was in "auto-discover" mode and it's the first host for which we have a DC, - // use it. - if (localDc == UNSET && dc != UNSET) { - logger.info( - "Using data-center name '{}' for DCAwareRoundRobinPolicy (if this is incorrect, please provide the correct datacenter name with DCAwareRoundRobinPolicy constructor)", - dc); - localDc = dc; - } - - CopyOnWriteArrayList dcHosts = perDcLiveHosts.get(dc); - if (dcHosts == null) { - CopyOnWriteArrayList newMap = - new CopyOnWriteArrayList(Collections.singletonList(host)); - dcHosts = perDcLiveHosts.putIfAbsent(dc, newMap); - // If we've successfully put our new host, we're good, otherwise we've been beaten so continue - if (dcHosts == null) return; - } - dcHosts.addIfAbsent(host); - } - - @Override - public void onDown(Host host) { - CopyOnWriteArrayList dcHosts = perDcLiveHosts.get(dc(host)); - if (dcHosts != null) dcHosts.remove(host); - } - - @Override - public void onAdd(Host host) { - onUp(host); - } - - @Override - public void onRemove(Host host) { - onDown(host); - } - - @Override - public void close() { - // nothing to do - } - - /** Helper class to build the policy. */ - public static class Builder { - private String localDc; - private int usedHostsPerRemoteDc; - private boolean allowRemoteDCsForLocalConsistencyLevel; - - /** - * Sets the name of the datacenter that will be considered "local" by the policy. - * - *

This must be the name as known by Cassandra (in other words, the name in that appears in - * {@code system.peers}, or in the output of admin tools like nodetool). - * - *

If this method isn't called, the policy will default to the datacenter of the first node - * connected to. This will always be ok if all the contact points use at {@code Cluster} - * creation are in the local data-center. Otherwise, you should provide the name yourself with - * this method. - * - * @param localDc the name of the datacenter. It should not be {@code null}. - * @return this builder. - */ - public Builder withLocalDc(String localDc) { - Preconditions.checkArgument( - !Strings.isNullOrEmpty(localDc), - "localDc name can't be null or empty. If you want to let the policy autodetect the datacenter, don't call Builder.withLocalDC"); - this.localDc = localDc; - return this; - } - - /** - * Sets the number of hosts per remote datacenter that the policy should consider. - * - *

The policy's {@code distance()} method will return a {@code HostDistance.REMOTE} distance - * for only {@code usedHostsPerRemoteDc} hosts per remote datacenter. Other hosts of the remote - * datacenters will be ignored (and thus no connections to them will be maintained). - * - *

If {@code usedHostsPerRemoteDc > 0}, then if for a query no host in the local datacenter - * can be reached and if the consistency level of the query is not {@code LOCAL_ONE} or {@code - * LOCAL_QUORUM}, then up to {@code usedHostsPerRemoteDc} hosts per remote datacenter will be - * tried by the policy as a fallback. By default, no remote host will be used for {@code - * LOCAL_ONE} and {@code LOCAL_QUORUM}, since this would change the meaning of the consistency - * level, somewhat breaking the consistency contract (this can be overridden with {@link - * #allowRemoteDCsForLocalConsistencyLevel()}). - * - *

If this method isn't called, the policy will default to 0. - * - * @param usedHostsPerRemoteDc the number. - * @return this builder. - * @deprecated This functionality will be removed in the next major release of the driver. DC - * failover shouldn't be done in the driver, which does not have the necessary context to - * know what makes sense considering application semantics. - */ - @Deprecated - public Builder withUsedHostsPerRemoteDc(int usedHostsPerRemoteDc) { - Preconditions.checkArgument( - usedHostsPerRemoteDc >= 0, "usedHostsPerRemoteDc must be equal or greater than 0"); - this.usedHostsPerRemoteDc = usedHostsPerRemoteDc; - return this; - } - - /** - * Allows the policy to return remote hosts when building query plans for queries having - * consistency level {@code LOCAL_ONE} or {@code LOCAL_QUORUM}. - * - *

When used in conjunction with {@link #withUsedHostsPerRemoteDc(int) usedHostsPerRemoteDc} - * > 0, this overrides the policy of never using remote datacenter nodes for {@code LOCAL_ONE} - * and {@code LOCAL_QUORUM} queries. It is however inadvisable to do so in almost all cases, as - * this would potentially break consistency guarantees and if you are fine with that, it's - * probably better to use a weaker consistency like {@code ONE}, {@code TWO} or {@code THREE}. - * As such, this method should generally be avoided; use it only if you know and understand what - * you do. - * - * @return this builder. - * @deprecated This functionality will be removed in the next major release of the driver. DC - * failover shouldn't be done in the driver, which does not have the necessary context to - * know what makes sense considering application semantics. - */ - @Deprecated - public Builder allowRemoteDCsForLocalConsistencyLevel() { - this.allowRemoteDCsForLocalConsistencyLevel = true; - return this; - } - - /** - * Builds the policy configured by this builder. - * - * @return the policy. - */ - public DCAwareRoundRobinPolicy build() { - if (usedHostsPerRemoteDc == 0 && allowRemoteDCsForLocalConsistencyLevel) { - logger.warn( - "Setting allowRemoteDCsForLocalConsistencyLevel has no effect if usedHostsPerRemoteDc = 0. " - + "This setting will be ignored"); - } - return new DCAwareRoundRobinPolicy( - localDc, usedHostsPerRemoteDc, allowRemoteDCsForLocalConsistencyLevel, true); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/DefaultRetryPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/DefaultRetryPolicy.java deleted file mode 100644 index dc8fed4262b..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/DefaultRetryPolicy.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.WriteType; -import com.datastax.driver.core.exceptions.DriverException; -import com.datastax.driver.core.exceptions.ReadFailureException; -import com.datastax.driver.core.exceptions.WriteFailureException; - -/** - * The default retry policy. - * - *

This policy retries queries in only two cases: - * - *

    - *
  • On a read timeout, retries once on the same host if enough replicas replied but data was - * not retrieved. - *
  • On a write timeout, retries once on the same host if we timeout while writing the - * distributed log used by batch statements. - *
  • On an unavailable exception, retries once on the next host. - *
  • On a request error, such as a client timeout, the query is retried on the next host. Do not - * retry on read or write failures. - *
- * - *

This retry policy is conservative in that it will never retry with a different consistency - * level than the one of the initial operation. - * - *

In some cases, it may be convenient to use a more aggressive retry policy like {@link - * DowngradingConsistencyRetryPolicy}. - */ -public class DefaultRetryPolicy implements RetryPolicy { - - public static final DefaultRetryPolicy INSTANCE = new DefaultRetryPolicy(); - - private DefaultRetryPolicy() {} - - /** - * {@inheritDoc} - * - *

This implementation triggers a maximum of one retry, and only if enough replicas had - * responded to the read request but data was not retrieved amongst those. Indeed, that case - * usually means that enough replica are alive to satisfy the consistency but the coordinator - * picked a dead one for data retrieval, not having detected that replica as dead yet. The - * reasoning for retrying then is that by the time we get the timeout the dead replica will likely - * have been detected as dead and the retry has a high chance of success. - * - * @return {@code RetryDecision.retry(cl)} if no retry attempt has yet been tried and {@code - * receivedResponses >= requiredResponses && !dataRetrieved}, {@code RetryDecision.rethrow()} - * otherwise. - */ - @Override - public RetryDecision onReadTimeout( - Statement statement, - ConsistencyLevel cl, - int requiredResponses, - int receivedResponses, - boolean dataRetrieved, - int nbRetry) { - if (nbRetry != 0) return RetryDecision.rethrow(); - - return receivedResponses >= requiredResponses && !dataRetrieved - ? RetryDecision.retry(cl) - : RetryDecision.rethrow(); - } - - /** - * {@inheritDoc} - * - *

This implementation triggers a maximum of one retry, and only in the case of a {@code - * WriteType.BATCH_LOG} write. The reasoning for the retry in that case is that write to the - * distributed batch log is tried by the coordinator of the write against a small subset of all - * the nodes alive in the local datacenter. Hence, a timeout usually means that none of the nodes - * in that subset were alive but the coordinator hasn't detected them as dead. By the time we get - * the timeout the dead nodes will likely have been detected as dead and the retry has thus a high - * chance of success. - * - * @return {@code RetryDecision.retry(cl)} if no retry attempt has yet been tried and {@code - * writeType == WriteType.BATCH_LOG}, {@code RetryDecision.rethrow()} otherwise. - */ - @Override - public RetryDecision onWriteTimeout( - Statement statement, - ConsistencyLevel cl, - WriteType writeType, - int requiredAcks, - int receivedAcks, - int nbRetry) { - if (nbRetry != 0) return RetryDecision.rethrow(); - - // If the batch log write failed, retry the operation as this might just be we were unlucky at - // picking candidates - // JAVA-764: testing the write type automatically filters out serial consistency levels as these - // have always WriteType.CAS. - return writeType == WriteType.BATCH_LOG ? RetryDecision.retry(cl) : RetryDecision.rethrow(); - } - - /** - * {@inheritDoc} - * - *

This implementation does the following: - * - *

    - *
  • if this is the first retry ({@code nbRetry == 0}), it triggers a retry on the next host - * in the query plan with the same consistency level ({@link - * RetryPolicy.RetryDecision#tryNextHost(ConsistencyLevel) RetryDecision#tryNextHost(null)}. - * The rationale is that the first coordinator might have been network-isolated from all - * other nodes (thinking they're down), but still able to communicate with the client; in - * that case, retrying on the same host has almost no chance of success, but moving to the - * next host might solve the issue. - *
  • otherwise, the exception is rethrow. - *
- */ - @Override - public RetryDecision onUnavailable( - Statement statement, - ConsistencyLevel cl, - int requiredReplica, - int aliveReplica, - int nbRetry) { - return (nbRetry == 0) ? RetryDecision.tryNextHost(null) : RetryDecision.rethrow(); - } - - /** {@inheritDoc} */ - @Override - public RetryDecision onRequestError( - Statement statement, ConsistencyLevel cl, DriverException e, int nbRetry) { - // do not retry these by default as they generally indicate a data problem or - // other issue that is unlikely to be resolved by a retry. - if (e instanceof WriteFailureException || e instanceof ReadFailureException) { - return RetryDecision.rethrow(); - } - return RetryDecision.tryNextHost(cl); - } - - @Override - public void init(Cluster cluster) { - // nothing to do - } - - @Override - public void close() { - // nothing to do - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/DowngradingConsistencyRetryPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/DowngradingConsistencyRetryPolicy.java deleted file mode 100644 index 27eaf72066c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/DowngradingConsistencyRetryPolicy.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.WriteType; -import com.datastax.driver.core.exceptions.DriverException; -import com.datastax.driver.core.exceptions.ReadFailureException; -import com.datastax.driver.core.exceptions.WriteFailureException; - -/** - * A retry policy that sometimes retries with a lower consistency level than the one initially - * requested. - * - *

BEWARE: this policy may retry queries using a lower consistency level than the one - * initially requested. By doing so, it may break consistency guarantees. In other words, if you use - * this retry policy, there are cases (documented below) where a read at {@code QUORUM} may - * not see a preceding write at {@code QUORUM}. Do not use this policy unless you have - * understood the cases where this can happen and are ok with that. It is also highly recommended to - * always wrap this policy into {@link LoggingRetryPolicy} to log the occurrences of such - * consistency breaks. - * - *

This policy implements the same retries than the {@link DefaultRetryPolicy} policy. But on top - * of that, it also retries in the following cases: - * - *

    - *
  • On a read timeout: if the number of replicas that responded is greater than one, but lower - * than is required by the requested consistency level, the operation is retried at a lower - * consistency level. - *
  • On a write timeout: if the operation is a {@code WriteType.UNLOGGED_BATCH} and at least one - * replica acknowledged the write, the operation is retried at a lower consistency level. - * Furthermore, for other operations, if at least one replica acknowledged the write, the - * timeout is ignored. - *
  • On an unavailable exception: if at least one replica is alive, the operation is retried at - * a lower consistency level. - *
- * - * The lower consistency level to use for retries is determined by the following rules: - * - *
    - *
  • if more than 3 replicas responded, use {@code THREE}. - *
  • if 1, 2 or 3 replicas responded, use the corresponding level {@code ONE}, {@code TWO} or - * {@code THREE}. - *
- * - * Note that if the initial consistency level was {@code EACH_QUORUM}, Cassandra returns the number - * of live replicas in the datacenter that failed to reach consistency, not the overall - * number in the cluster. Therefore if this number is 0, we still retry at {@code ONE}, on the - * assumption that a host may still be up in another datacenter. - * - *

The reasoning being this retry policy is the following one. If, based on the information the - * Cassandra coordinator node returns, retrying the operation with the initially requested - * consistency has a chance to succeed, do it. Otherwise, if based on this information we know - * the initially requested consistency level cannot be achieved currently, then: - * - *

    - *
  • For writes, ignore the exception (thus silently failing the consistency requirement) if we - * know the write has been persisted on at least one replica. - *
  • For reads, try reading at a lower consistency level (thus silently failing the consistency - * requirement). - *
- * - * In other words, this policy implements the idea that if the requested consistency level cannot be - * achieved, the next best thing for writes is to make sure the data is persisted, and that reading - * something is better than reading nothing, even if there is a risk of reading stale data. - * - * @deprecated as of version 3.5.0, this retry policy has been deprecated, and it will be removed in - * 4.0.0. See our upgrade - * guide to understand how to migrate existing applications that rely on this policy. - */ -@Deprecated -@SuppressWarnings("DeprecatedIsStillUsed") -public class DowngradingConsistencyRetryPolicy implements RetryPolicy { - - public static final DowngradingConsistencyRetryPolicy INSTANCE = - new DowngradingConsistencyRetryPolicy(); - - private DowngradingConsistencyRetryPolicy() {} - - private RetryDecision maxLikelyToWorkCL(int knownOk, ConsistencyLevel currentCL) { - if (knownOk >= 3) return RetryDecision.retry(ConsistencyLevel.THREE); - - if (knownOk == 2) return RetryDecision.retry(ConsistencyLevel.TWO); - - // JAVA-1005: EACH_QUORUM does not report a global number of alive replicas - // so even if we get 0 alive replicas, there might be - // a node up in some other datacenter - if (knownOk == 1 || currentCL == ConsistencyLevel.EACH_QUORUM) - return RetryDecision.retry(ConsistencyLevel.ONE); - - return RetryDecision.rethrow(); - } - - /** - * {@inheritDoc} - * - *

This implementation triggers a maximum of one retry. If less replicas responded than - * required by the consistency level (but at least one replica did respond), the operation is - * retried at a lower consistency level. If enough replicas responded but data was not retrieved, - * the operation is retried with the initial consistency level. Otherwise, an exception is thrown. - */ - @Override - public RetryDecision onReadTimeout( - Statement statement, - ConsistencyLevel cl, - int requiredResponses, - int receivedResponses, - boolean dataRetrieved, - int nbRetry) { - if (nbRetry != 0) return RetryDecision.rethrow(); - - // CAS reads are not all that useful in terms of visibility of the writes since CAS write - // supports the - // normal consistency levels on the committing phase. So the main use case for CAS reads is - // probably for - // when you've timed out on a CAS write and want to make sure what happened. Downgrading in that - // case - // would be always wrong so we just special case to rethrow. - if (cl.isSerial()) return RetryDecision.rethrow(); - - if (receivedResponses < requiredResponses) { - // Tries the biggest CL that is expected to work - return maxLikelyToWorkCL(receivedResponses, cl); - } - - return !dataRetrieved ? RetryDecision.retry(cl) : RetryDecision.rethrow(); - } - - /** - * {@inheritDoc} - * - *

This implementation triggers a maximum of one retry. If {@code writeType == - * WriteType.BATCH_LOG}, the write is retried with the initial consistency level. If {@code - * writeType == WriteType.UNLOGGED_BATCH} and at least one replica acknowledged, the write is - * retried with a lower consistency level (with unlogged batch, a write timeout can always - * mean that part of the batch haven't been persisted at all, even if {@code receivedAcks > 0}). - * For other write types ({@code WriteType.SIMPLE} and {@code WriteType.BATCH}), if we know the - * write has been persisted on at least one replica, we ignore the exception. Otherwise, an - * exception is thrown. - */ - @Override - public RetryDecision onWriteTimeout( - Statement statement, - ConsistencyLevel cl, - WriteType writeType, - int requiredAcks, - int receivedAcks, - int nbRetry) { - if (nbRetry != 0) return RetryDecision.rethrow(); - - switch (writeType) { - case SIMPLE: - case BATCH: - // Since we provide atomicity there is no point in retrying - return receivedAcks > 0 ? RetryDecision.ignore() : RetryDecision.rethrow(); - case UNLOGGED_BATCH: - // Since only part of the batch could have been persisted, - // retry with whatever consistency should allow to persist all - return maxLikelyToWorkCL(receivedAcks, cl); - case BATCH_LOG: - return RetryDecision.retry(cl); - } - // We want to rethrow on COUNTER and CAS, because in those case "we don't know" and don't want - // to guess - return RetryDecision.rethrow(); - } - - /** - * {@inheritDoc} - * - *

This implementation triggers a maximum of one retry. If at least one replica is known to be - * alive, the operation is retried at a lower consistency level. - */ - @Override - public RetryDecision onUnavailable( - Statement statement, - ConsistencyLevel cl, - int requiredReplica, - int aliveReplica, - int nbRetry) { - if (nbRetry != 0) return RetryDecision.rethrow(); - - // JAVA-764: if the requested consistency level is serial, it means that the operation failed at - // the paxos phase of a LWT. - // Retry on the next host, on the assumption that the initial coordinator could be - // network-isolated. - if (cl.isSerial()) return RetryDecision.tryNextHost(null); - - // Tries the biggest CL that is expected to work - return maxLikelyToWorkCL(aliveReplica, cl); - } - - /** - * {@inheritDoc} - * - *

For historical reasons, this implementation triggers a retry on the next host in the query - * plan with the same consistency level, regardless of the statement's idempotence. Note that this - * breaks the general rule stated in {@link RetryPolicy#onRequestError(Statement, - * ConsistencyLevel, DriverException, int)}: "a retry should only be attempted if the request is - * known to be idempotent".` - */ - @Override - public RetryDecision onRequestError( - Statement statement, ConsistencyLevel cl, DriverException e, int nbRetry) { - // do not retry these by default as they generally indicate a data problem or - // other issue that is unlikely to be resolved by a retry. - if (e instanceof WriteFailureException || e instanceof ReadFailureException) { - return RetryDecision.rethrow(); - } - return RetryDecision.tryNextHost(cl); - } - - @Override - public void init(Cluster cluster) { - // nothing to do - } - - @Override - public void close() { - // nothing to do - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/EC2MultiRegionAddressTranslator.java b/driver-core/src/main/java/com/datastax/driver/core/policies/EC2MultiRegionAddressTranslator.java deleted file mode 100644 index 4507d4d425e..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/EC2MultiRegionAddressTranslator.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.exceptions.DriverException; -import com.google.common.annotations.VisibleForTesting; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.util.Enumeration; -import java.util.Hashtable; -import javax.naming.Context; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.InitialDirContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@link AddressTranslator} implementation for a multi-region EC2 deployment where clients are - * also deployed in EC2. - * - *

Its distinctive feature is that it translates addresses according to the location of the - * Cassandra host: - * - *

    - *
  • addresses in different EC2 regions (than the client) are unchanged; - *
  • addresses in the same EC2 region are translated to private IPs. - *
- * - * This optimizes network costs, because Amazon charges more for communication over public IPs. - * - *

- * - *

Implementation note: this class performs a reverse DNS lookup of the origin address, to find - * the domain name of the target instance. Then it performs a forward DNS lookup of the domain name; - * the EC2 DNS does the private/public switch automatically based on location. - */ -public class EC2MultiRegionAddressTranslator implements AddressTranslator { - - private static final Logger logger = - LoggerFactory.getLogger(EC2MultiRegionAddressTranslator.class); - - // TODO when we switch to Netty 4.1, we can replace this with the Netty built-in DNS client - private final DirContext ctx; - - public EC2MultiRegionAddressTranslator() { - Hashtable env = new Hashtable(); - env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); - try { - ctx = new InitialDirContext(env); - } catch (NamingException e) { - throw new DriverException("Could not create translator", e); - } - } - - @VisibleForTesting - EC2MultiRegionAddressTranslator(DirContext ctx) { - this.ctx = ctx; - } - - @Override - public void init(Cluster cluster) { - // nothing to do - } - - @Override - public InetSocketAddress translate(InetSocketAddress socketAddress) { - InetAddress address = socketAddress.getAddress(); - try { - // InetAddress#getHostName() is supposed to perform a reverse DNS lookup, but for some reason - // it doesn't work - // within the same EC2 region (it returns the IP address itself). - // We use an alternate implementation: - String domainName = lookupPtrRecord(reverse(address)); - if (domainName == null) { - logger.warn("Found no domain name for {}, returning it as-is", address); - return socketAddress; - } - - InetAddress translatedAddress = InetAddress.getByName(domainName); - logger.debug("Resolved {} to {}", address, translatedAddress); - return new InetSocketAddress(translatedAddress, socketAddress.getPort()); - } catch (Exception e) { - logger.warn("Error resolving " + address + ", returning it as-is", e); - return socketAddress; - } - } - - private String lookupPtrRecord(String reversedDomain) throws Exception { - Attributes attrs = ctx.getAttributes(reversedDomain, new String[] {"PTR"}); - for (NamingEnumeration ae = attrs.getAll(); ae.hasMoreElements(); ) { - Attribute attr = (Attribute) ae.next(); - for (Enumeration vals = attr.getAll(); vals.hasMoreElements(); ) - return vals.nextElement().toString(); - } - return null; - } - - @Override - public void close() { - try { - ctx.close(); - } catch (NamingException e) { - logger.warn("Error closing translator", e); - } - } - - // Builds the "reversed" domain name in the ARPA domain to perform the reverse lookup - @VisibleForTesting - static String reverse(InetAddress address) { - byte[] bytes = address.getAddress(); - if (bytes.length == 4) return reverseIpv4(bytes); - else return reverseIpv6(bytes); - } - - private static String reverseIpv4(byte[] bytes) { - StringBuilder builder = new StringBuilder(); - for (int i = bytes.length - 1; i >= 0; i--) { - builder.append(bytes[i] & 0xFF).append('.'); - } - builder.append("in-addr.arpa"); - return builder.toString(); - } - - private static String reverseIpv6(byte[] bytes) { - StringBuilder builder = new StringBuilder(); - for (int i = bytes.length - 1; i >= 0; i--) { - byte b = bytes[i]; - int lowNibble = b & 0x0F; - int highNibble = b >> 4 & 0x0F; - builder - .append(Integer.toHexString(lowNibble)) - .append('.') - .append(Integer.toHexString(highNibble)) - .append('.'); - } - builder.append("ip6.arpa"); - return builder.toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/ErrorAwarePolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/ErrorAwarePolicy.java deleted file mode 100644 index 7d6992ccfd4..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/ErrorAwarePolicy.java +++ /dev/null @@ -1,381 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import static java.util.concurrent.TimeUnit.MINUTES; -import static java.util.concurrent.TimeUnit.NANOSECONDS; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.HostDistance; -import com.datastax.driver.core.LatencyTracker; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.exceptions.AlreadyExistsException; -import com.datastax.driver.core.exceptions.FunctionExecutionException; -import com.datastax.driver.core.exceptions.InvalidQueryException; -import com.datastax.driver.core.exceptions.QueryConsistencyException; -import com.datastax.driver.core.exceptions.SyntaxError; -import com.datastax.driver.core.exceptions.UnavailableException; -import com.google.common.annotations.Beta; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.AbstractIterator; -import com.google.common.collect.ImmutableList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Chainable load balancing policy that filters out hosts based on their error rates. - * - *

When creating a query plan, this policy gathers a list of candidate hosts from its child - * policy; for each candidate host, it then determines whether it should be included into or - * excluded from the final query plan, based on its current error rate (measured over the last - * minute, with a 5-second granularity). - * - *

Note that the policy should not blindly count all errors in its measurements: some type of - * errors (e.g. CQL syntax errors) can originate from the client and occur on all hosts, therefore - * they should not count towards the exclusion threshold or all hosts could become excluded. You can - * provide your own {@link ErrorFilter} to customize that logic. - * - *

The policy follows the builder pattern to be created, the {@link Builder} class can be created - * with {@link #builder} method. - * - *

This policy is currently in BETA mode and its behavior might be changing throughout different - * driver versions. - */ -@Beta -public class ErrorAwarePolicy implements ChainableLoadBalancingPolicy { - - private static final Logger logger = LoggerFactory.getLogger(ErrorAwarePolicy.class); - - private final LoadBalancingPolicy childPolicy; - - private final long retryPeriodNanos; - - PerHostErrorTracker errorTracker; - - private ErrorAwarePolicy(Builder builder) { - this.childPolicy = builder.childPolicy; - this.retryPeriodNanos = builder.retryPeriodNanos; - this.errorTracker = - new PerHostErrorTracker(builder.maxErrorsPerMinute, builder.errorFilter, builder.clock); - } - - @Override - public LoadBalancingPolicy getChildPolicy() { - return childPolicy; - } - - @Override - public void init(Cluster cluster, Collection hosts) { - childPolicy.init(cluster, hosts); - cluster.register(this.errorTracker); - } - - @Override - public HostDistance distance(Host host) { - return childPolicy.distance(host); - } - - @Override - public Iterator newQueryPlan(String loggedKeyspace, Statement statement) { - final Iterator childQueryPlan = childPolicy.newQueryPlan(loggedKeyspace, statement); - - return new AbstractIterator() { - - @Override - protected Host computeNext() { - while (childQueryPlan.hasNext()) { - Host host = childQueryPlan.next(); - if (!errorTracker.isExcluded(host)) { - return host; - } - } - return endOfData(); - } - }; - } - - @Override - public void onAdd(Host host) { - childPolicy.onAdd(host); - } - - @Override - public void onUp(Host host) { - childPolicy.onUp(host); - } - - @Override - public void onDown(Host host) { - childPolicy.onDown(host); - } - - @Override - public void onRemove(Host host) { - childPolicy.onRemove(host); - } - - /** - * Creates a new error aware policy builder given the child policy that the resulting policy - * should wrap. - * - * @param childPolicy the load balancing policy to wrap with error awareness. - * @return the created builder. - */ - public static Builder builder(LoadBalancingPolicy childPolicy) { - return new Builder(childPolicy); - } - - @Override - public void close() { - childPolicy.close(); - } - - /** Utility class to create a {@link ErrorAwarePolicy}. */ - public static class Builder { - final LoadBalancingPolicy childPolicy; - - private int maxErrorsPerMinute = 1; - private long retryPeriodNanos = NANOSECONDS.convert(2, MINUTES); - private Clock clock = Clock.DEFAULT; - - private ErrorFilter errorFilter = new DefaultErrorFilter(); - - /** - * Creates a {@link Builder} instance. - * - * @param childPolicy the load balancing policy to wrap with error awareness. - */ - public Builder(LoadBalancingPolicy childPolicy) { - this.childPolicy = childPolicy; - } - - /** - * Defines the maximum number of errors allowed per minute for each host. - * - *

The policy keeps track of the number of errors on each host (filtered by {@link - * Builder#withErrorsFilter(com.datastax.driver.core.policies.ErrorAwarePolicy.ErrorFilter)}) - * over a sliding 1-minute window. If a host had more than this number of errors, it will be - * excluded from the query plan for the duration defined by {@link #withRetryPeriod(long, - * TimeUnit)}. - * - *

Default value for the threshold is 1. - * - * @param maxErrorsPerMinute the number. - * @return this {@link Builder} instance, for method chaining. - */ - public Builder withMaxErrorsPerMinute(int maxErrorsPerMinute) { - this.maxErrorsPerMinute = maxErrorsPerMinute; - return this; - } - - /** - * Defines the time during which a host is excluded by the policy once it has exceeded {@link - * #withMaxErrorsPerMinute(int)}. - * - *

Default value for the retry period is 2 minutes. - * - * @param retryPeriod the period of exclusion for a host. - * @param retryPeriodTimeUnit the time unit for the retry period. - * @return this {@link Builder} instance, for method chaining. - */ - public Builder withRetryPeriod(long retryPeriod, TimeUnit retryPeriodTimeUnit) { - this.retryPeriodNanos = retryPeriodTimeUnit.toNanos(retryPeriod); - return this; - } - - /** - * Provides a filter that will decide which errors are counted towards {@link - * #withMaxErrorsPerMinute(int)}. - * - *

The default implementation will exclude from the error counting, the following exception - * types: - * - *

    - *
  • {@link QueryConsistencyException} and {@link UnavailableException}: the assumption is - * that these errors are most often caused by other replicas being unavailable, not by - * something wrong on the coordinator; - *
  • {@link InvalidQueryException}, {@link AlreadyExistsException}, {@link SyntaxError}: - * these are likely caused by a bad query in client code, that will fail on all hosts. - * Excluding hosts could lead to complete loss of connectivity, rather the solution is to - * fix the query; - *
  • {@link FunctionExecutionException}: similarly, this is caused by a bad function - * definition and likely to fail on all hosts. - *
- * - * @param errorFilter the filter class that the policy will use. - * @return this {@link Builder} instance, for method chaining. - */ - public Builder withErrorsFilter(ErrorFilter errorFilter) { - this.errorFilter = errorFilter; - return this; - } - - @VisibleForTesting - Builder withClock(Clock clock) { - this.clock = clock; - return this; - } - - /** - * Creates the {@link ErrorAwarePolicy} instance. - * - * @return the newly created {@link ErrorAwarePolicy}. - */ - public ErrorAwarePolicy build() { - return new ErrorAwarePolicy(this); - } - } - - class PerHostErrorTracker implements LatencyTracker { - - private final int maxErrorsPerMinute; - private final ErrorFilter errorFilter; - private final Clock clock; - private final ConcurrentMap hostsCounts = - new ConcurrentHashMap(); - private final ConcurrentMap exclusionTimes = new ConcurrentHashMap(); - - PerHostErrorTracker(int maxErrorsPerMinute, ErrorFilter errorFilter, Clock clock) { - this.maxErrorsPerMinute = maxErrorsPerMinute; - this.errorFilter = errorFilter; - this.clock = clock; - } - - @Override - public void update(Host host, Statement statement, Exception exception, long newLatencyNanos) { - if (exception == null) { - return; - } - if (!errorFilter.shouldConsiderError(exception, host, statement)) { - return; - } - RollingCount hostCount = getOrCreateCount(host); - hostCount.increment(); - } - - boolean isExcluded(Host host) { - Long excludedTime = exclusionTimes.get(host); - boolean expired = excludedTime != null && clock.nanoTime() - excludedTime >= retryPeriodNanos; - if (excludedTime == null || expired) { - if (maybeExcludeNow(host, excludedTime)) { - return true; - } - if (expired) { - // Cleanup, but make sure we don't overwrite if another thread just set it - exclusionTimes.remove(host, excludedTime); - } - return false; - } else { // host is already excluded - return true; - } - } - - // Exclude if we're over the threshold - private boolean maybeExcludeNow(Host host, Long previousTime) { - RollingCount rollingCount = getOrCreateCount(host); - long count = rollingCount.get(); - if (count > maxErrorsPerMinute) { - excludeNow(host, count, previousTime); - return true; - } else { - return false; - } - } - - // Set the exclusion time to now, handling potential races - private void excludeNow(Host host, long count, Long previousTime) { - long now = clock.nanoTime(); - boolean didNotRace = - (previousTime == null) - ? exclusionTimes.putIfAbsent(host, now) == null - : exclusionTimes.replace(host, previousTime, now); - - if (didNotRace && logger.isDebugEnabled()) { - logger.debug( - String.format( - "Host %s encountered %d errors in the last minute, which is more " - + "than the maximum allowed (%d). It will be excluded from query plans for the " - + "next %d nanoseconds.", - host, count, maxErrorsPerMinute, retryPeriodNanos)); - } - } - - private RollingCount getOrCreateCount(Host host) { - RollingCount hostCount = hostsCounts.get(host); - if (hostCount == null) { - RollingCount tmp = new RollingCount(clock); - hostCount = hostsCounts.putIfAbsent(host, tmp); - if (hostCount == null) hostCount = tmp; - } - return hostCount; - } - - @Override - public void onRegister(Cluster cluster) { - // nothing to do. - } - - @Override - public void onUnregister(Cluster cluster) { - // nothing to do. - } - } - - static class DefaultErrorFilter implements ErrorFilter { - private static final List> IGNORED_EXCEPTIONS = - ImmutableList.>builder() - .add(FunctionExecutionException.class) - .add(QueryConsistencyException.class) - .add(UnavailableException.class) - .add(AlreadyExistsException.class) - .add(InvalidQueryException.class) - .add(SyntaxError.class) - .build(); - - @Override - public boolean shouldConsiderError(Exception e, Host host, Statement statement) { - for (Class ignoredException : IGNORED_EXCEPTIONS) { - if (ignoredException.isInstance(e)) return false; - } - return true; - } - } - - /** - * A filter for the errors considered by {@link ErrorAwarePolicy}. - * - *

Only errors that indicate something wrong with a host should lead to its exclusion from - * query plans. - */ - public interface ErrorFilter { - /** - * Whether an error should be counted in the host's error rate. - * - * @param e the exception. - * @param host the host. - * @param statement the statement that caused the exception. - * @return {@code true} if the exception should be counted. - */ - boolean shouldConsiderError(Exception e, Host host, Statement statement); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/ExponentialReconnectionPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/ExponentialReconnectionPolicy.java deleted file mode 100644 index 0b4282a98ac..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/ExponentialReconnectionPolicy.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; - -/** - * A reconnection policy that waits exponentially longer between each reconnection attempt (but - * keeps a constant delay once a maximum delay is reached). - */ -public class ExponentialReconnectionPolicy implements ReconnectionPolicy { - - private final long baseDelayMs; - private final long maxDelayMs; - private final long maxAttempts; - - /** - * Creates a reconnection policy waiting exponentially longer for each new attempt. - * - * @param baseDelayMs the base delay in milliseconds to use for the schedules created by this - * policy. - * @param maxDelayMs the maximum delay to wait between two attempts. - */ - public ExponentialReconnectionPolicy(long baseDelayMs, long maxDelayMs) { - if (baseDelayMs < 0 || maxDelayMs < 0) - throw new IllegalArgumentException("Invalid negative delay"); - if (baseDelayMs == 0) - throw new IllegalArgumentException("baseDelayMs must be strictly positive"); - if (maxDelayMs < baseDelayMs) - throw new IllegalArgumentException( - String.format( - "maxDelayMs (got %d) cannot be smaller than baseDelayMs (got %d)", - maxDelayMs, baseDelayMs)); - - this.baseDelayMs = baseDelayMs; - this.maxDelayMs = maxDelayMs; - - // Maximum number of attempts after which we overflow (which is kind of theoretical anyway, - // you'll - // die of old age before reaching that but hey ...) - int ceil = (baseDelayMs & (baseDelayMs - 1)) == 0 ? 0 : 1; - this.maxAttempts = 64 - Long.numberOfLeadingZeros(Long.MAX_VALUE / baseDelayMs) - ceil; - } - - /** - * The base delay in milliseconds for this policy (e.g. the delay before the first reconnection - * attempt). - * - * @return the base delay in milliseconds for this policy. - */ - public long getBaseDelayMs() { - return baseDelayMs; - } - - /** - * The maximum delay in milliseconds between reconnection attempts for this policy. - * - * @return the maximum delay in milliseconds between reconnection attempts for this policy. - */ - public long getMaxDelayMs() { - return maxDelayMs; - } - - /** - * A new schedule that used an exponentially growing delay between reconnection attempts. - * - *

For this schedule, reconnection attempt {@code i} will be tried {@code Math.min(2^(i-1) * - * getBaseDelayMs(), getMaxDelayMs())} milliseconds after the previous one. - * - * @return the newly created schedule. - */ - @Override - public ReconnectionSchedule newSchedule() { - return new ExponentialSchedule(); - } - - private class ExponentialSchedule implements ReconnectionSchedule { - - private int attempts; - - @Override - public long nextDelayMs() { - - if (attempts > maxAttempts) return maxDelayMs; - - return Math.min(baseDelayMs * (1L << attempts++), maxDelayMs); - } - } - - @Override - public void init(Cluster cluster) { - // nothing to do - } - - @Override - public void close() { - // nothing to do - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/FallthroughRetryPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/FallthroughRetryPolicy.java deleted file mode 100644 index 22c6f030c8c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/FallthroughRetryPolicy.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.WriteType; -import com.datastax.driver.core.exceptions.DriverException; - -/** - * A retry policy that never retries (nor ignores). - * - *

All of the methods of this retry policy unconditionally return {@link - * RetryPolicy.RetryDecision#rethrow()}. If this policy is used, retry logic will have to be - * implemented in business code. - */ -public class FallthroughRetryPolicy implements RetryPolicy { - - public static final FallthroughRetryPolicy INSTANCE = new FallthroughRetryPolicy(); - - private FallthroughRetryPolicy() {} - - /** - * {@inheritDoc} - * - *

This implementation always returns {@code RetryDecision.rethrow()}. - */ - @Override - public RetryDecision onReadTimeout( - Statement statement, - ConsistencyLevel cl, - int requiredResponses, - int receivedResponses, - boolean dataRetrieved, - int nbRetry) { - return RetryDecision.rethrow(); - } - - /** - * {@inheritDoc} - * - *

This implementation always returns {@code RetryDecision.rethrow()}. - */ - @Override - public RetryDecision onWriteTimeout( - Statement statement, - ConsistencyLevel cl, - WriteType writeType, - int requiredAcks, - int receivedAcks, - int nbRetry) { - return RetryDecision.rethrow(); - } - - /** - * {@inheritDoc} - * - *

This implementation always returns {@code RetryDecision.rethrow()}. - */ - @Override - public RetryDecision onUnavailable( - Statement statement, - ConsistencyLevel cl, - int requiredReplica, - int aliveReplica, - int nbRetry) { - return RetryDecision.rethrow(); - } - - /** - * {@inheritDoc} - * - *

This implementation always returns {@code RetryDecision.rethrow()}. - */ - @Override - public RetryDecision onRequestError( - Statement statement, ConsistencyLevel cl, DriverException e, int nbRetry) { - return RetryDecision.rethrow(); - } - - @Override - public void init(Cluster cluster) { - // nothing to do - } - - @Override - public void close() { - // nothing to do - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/HostFilterPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/HostFilterPolicy.java deleted file mode 100644 index 8bb1a5e6573..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/HostFilterPolicy.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.HostDistance; -import com.datastax.driver.core.Statement; -import com.google.common.base.Predicate; -import com.google.common.base.Predicates; -import com.google.common.collect.ImmutableSet; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -/** - * A load balancing policy wrapper that ensures that only hosts matching the predicate will ever be - * returned. - * - *

This policy wraps another load balancing policy and will delegate the choice of hosts to the - * wrapped policy with the exception that only hosts matching the predicate provided when - * constructing this policy will ever be returned. Any host not matching the predicate will be - * considered {@code IGNORED} and thus will not be connected to. - */ -public class HostFilterPolicy implements ChainableLoadBalancingPolicy { - private final LoadBalancingPolicy childPolicy; - private final Predicate predicate; - - /** - * Create a new policy that wraps the provided child policy but only "allows" hosts matching the - * predicate. - * - * @param childPolicy the wrapped policy. - * @param predicate the host predicate. Only hosts matching this predicate may get connected to - * (whether they will get connected to or not depends on the child policy). - */ - public HostFilterPolicy(LoadBalancingPolicy childPolicy, Predicate predicate) { - this.childPolicy = childPolicy; - this.predicate = predicate; - } - - @Override - public LoadBalancingPolicy getChildPolicy() { - return childPolicy; - } - - /** - * {@inheritDoc} - * - * @throws IllegalArgumentException if none of the host in {@code hosts} (which will correspond to - * the contact points) matches the predicate. - */ - @Override - public void init(Cluster cluster, Collection hosts) { - List whiteHosts = new ArrayList(hosts.size()); - for (Host host : hosts) if (predicate.apply(host)) whiteHosts.add(host); - - if (whiteHosts.isEmpty()) - throw new IllegalArgumentException( - String.format( - "Cannot use HostFilterPolicy where the filter allows none of the contacts points (%s)", - hosts)); - - childPolicy.init(cluster, whiteHosts); - } - - /** - * {@inheritDoc} - * - * @return {@link HostDistance#IGNORED} if {@code host} is not matching the predicate, the - * HostDistance as returned by the wrapped policy otherwise. - */ - @Override - public HostDistance distance(Host host) { - return predicate.apply(host) ? childPolicy.distance(host) : HostDistance.IGNORED; - } - - /** - * {@inheritDoc} - * - *

It is guaranteed that only hosts matching the predicate will be returned. - */ - @Override - public Iterator newQueryPlan(String loggedKeyspace, Statement statement) { - // Just delegate to the child policy, since we filter the hosts not white - // listed upfront, the child policy will never see a host that is not white - // listed and thus can't return one. - return childPolicy.newQueryPlan(loggedKeyspace, statement); - } - - @Override - public void onUp(Host host) { - if (predicate.apply(host)) childPolicy.onUp(host); - } - - @Override - public void onDown(Host host) { - if (predicate.apply(host)) childPolicy.onDown(host); - } - - @Override - public void onAdd(Host host) { - if (predicate.apply(host)) childPolicy.onAdd(host); - } - - @Override - public void onRemove(Host host) { - if (predicate.apply(host)) childPolicy.onRemove(host); - } - - @Override - public void close() { - childPolicy.close(); - } - - /** - * Create a new policy that wraps the provided child policy but only "allows" hosts whose DC - * belongs to the provided list. - * - * @param childPolicy the wrapped policy. - * @param dcs the DCs. - * @return the policy. - */ - public static HostFilterPolicy fromDCWhiteList( - LoadBalancingPolicy childPolicy, Iterable dcs) { - return new HostFilterPolicy(childPolicy, hostDCPredicate(dcs, true)); - } - - /** - * Create a new policy that wraps the provided child policy but only "forbids" hosts whose DC - * belongs to the provided list. - * - * @param childPolicy the wrapped policy. - * @param dcs the DCs. - * @return the policy. - */ - public static HostFilterPolicy fromDCBlackList( - LoadBalancingPolicy childPolicy, Iterable dcs) { - return new HostFilterPolicy(childPolicy, Predicates.not(hostDCPredicate(dcs, false))); - } - - private static Predicate hostDCPredicate( - Iterable dcs, final boolean includeNullDC) { - final ImmutableSet _dcs = ImmutableSet.copyOf(dcs); - return new Predicate() { - @Override - public boolean apply(Host host) { - String hdc = host.getDatacenter(); - return (hdc == null) ? includeNullDC : _dcs.contains(hdc); - } - }; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/IdempotenceAwareRetryPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/IdempotenceAwareRetryPolicy.java deleted file mode 100644 index b73e183a150..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/IdempotenceAwareRetryPolicy.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.QueryOptions; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.WriteType; -import com.datastax.driver.core.exceptions.DriverException; - -/** - * A retry policy that avoids retrying non-idempotent statements. - * - *

In case of write timeouts or unexpected errors, this policy will always return {@link - * com.datastax.driver.core.policies.RetryPolicy.RetryDecision#rethrow()} if the statement is deemed - * non-idempotent (see {@link #isIdempotent(Statement)}). - * - *

For all other cases, this policy delegates the decision to the child policy. - * - * @deprecated As of version 3.1.0, the driver doesn't retry non-idempotent statements for write - * timeouts or unexpected errors anymore. It is no longer necessary to wrap your retry policies - * in this policy. - */ -@Deprecated -public class IdempotenceAwareRetryPolicy implements RetryPolicy { - - private final RetryPolicy childPolicy; - - private QueryOptions queryOptions; - - /** - * Creates a new instance. - * - * @param childPolicy the policy to wrap. - */ - public IdempotenceAwareRetryPolicy(RetryPolicy childPolicy) { - this.childPolicy = childPolicy; - } - - @Override - public RetryDecision onReadTimeout( - Statement statement, - ConsistencyLevel cl, - int requiredResponses, - int receivedResponses, - boolean dataRetrieved, - int nbRetry) { - return childPolicy.onReadTimeout( - statement, cl, requiredResponses, receivedResponses, dataRetrieved, nbRetry); - } - - @Override - public RetryDecision onWriteTimeout( - Statement statement, - ConsistencyLevel cl, - WriteType writeType, - int requiredAcks, - int receivedAcks, - int nbRetry) { - if (isIdempotent(statement)) - return childPolicy.onWriteTimeout( - statement, cl, writeType, requiredAcks, receivedAcks, nbRetry); - else return RetryDecision.rethrow(); - } - - @Override - public RetryDecision onUnavailable( - Statement statement, - ConsistencyLevel cl, - int requiredReplica, - int aliveReplica, - int nbRetry) { - return childPolicy.onUnavailable(statement, cl, requiredReplica, aliveReplica, nbRetry); - } - - @Override - public RetryDecision onRequestError( - Statement statement, ConsistencyLevel cl, DriverException e, int nbRetry) { - if (isIdempotent(statement)) return childPolicy.onRequestError(statement, cl, e, nbRetry); - else return RetryDecision.rethrow(); - } - - @Override - public void init(Cluster cluster) { - childPolicy.init(cluster); - queryOptions = cluster.getConfiguration().getQueryOptions(); - } - - @Override - public void close() { - childPolicy.close(); - } - - /** - * Determines whether the given statement is idempotent or not. - * - *

The current implementation inspects the statement's {@link Statement#isIdempotent() - * idempotent flag}; if this flag is not set, then it inspects {@link - * QueryOptions#getDefaultIdempotence()}. - * - *

Subclasses may override if they have better knowledge of the statement being executed. - * - * @param statement The statement to execute. - * @return {@code true} if the given statement is idempotent, {@code false} otherwise - */ - protected boolean isIdempotent(Statement statement) { - Boolean myValue = statement.isIdempotent(); - if (myValue != null) return myValue; - else return queryOptions.getDefaultIdempotence(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/IdentityTranslator.java b/driver-core/src/main/java/com/datastax/driver/core/policies/IdentityTranslator.java deleted file mode 100644 index 9276a855860..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/IdentityTranslator.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import java.net.InetSocketAddress; - -/** The default {@link AddressTranslator} used by the driver that do no translation. */ -public class IdentityTranslator implements AddressTranslator { - - @Override - public void init(Cluster cluster) { - // Nothing to do - } - - /** - * Translates a Cassandra {@code rpc_address} to another address if necessary. - * - *

This method is the identity function, it always return the address passed in argument, doing - * no translation. - * - * @param address the address of a node as returned by Cassandra. - * @return {@code address} unmodified. - */ - @Override - public InetSocketAddress translate(InetSocketAddress address) { - return address; - } - - @Override - public void close() {} -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/LatencyAwarePolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/LatencyAwarePolicy.java deleted file mode 100644 index 34152601258..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/LatencyAwarePolicy.java +++ /dev/null @@ -1,837 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.codahale.metrics.Gauge; -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.HostDistance; -import com.datastax.driver.core.LatencyTracker; -import com.datastax.driver.core.Metrics; -import com.datastax.driver.core.MetricsUtil; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.exceptions.BootstrappingException; -import com.datastax.driver.core.exceptions.DriverException; -import com.datastax.driver.core.exceptions.OverloadedException; -import com.datastax.driver.core.exceptions.QueryValidationException; -import com.datastax.driver.core.exceptions.UnavailableException; -import com.datastax.driver.core.exceptions.UnpreparedException; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.AbstractIterator; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A wrapper load balancing policy that adds latency awareness to a child policy. - * - *

When used, this policy will collect the latencies of the queries to each Cassandra node and - * maintain a per-node average latency score. The nodes that are slower than the best performing - * node by more than a configurable threshold will be moved to the end of the query plan (that is, - * they will only be tried if all other nodes failed). Note that this policy only penalizes slow - * nodes, it does not globally sort the query plan by latency. - * - *

The latency score for a given node is a based on a form of exponential moving - * average. In other words, the latency score of a node is the average of its previously - * measured latencies, but where older measurements gets an exponentially decreasing weight. The - * exact weight applied to a newly received latency is based on the time elapsed since the previous - * measure (to account for the fact that latencies are not necessarily reported with equal - * regularity, neither over time nor between different nodes). - * - *

Once a node is excluded from query plans (because its averaged latency grew over the exclusion - * threshold), its latency score will not be updated anymore (since it is not queried). To give a - * chance to this node to recover, the policy has a configurable retry period. The policy will not - * penalize a host for which no measurement has been collected for more than this retry period. - * - *

Please see the {@link Builder} class and methods for more details on the possible parameters - * of this policy. - * - * @since 1.0.4 - */ -public class LatencyAwarePolicy implements ChainableLoadBalancingPolicy { - - private static final Logger logger = LoggerFactory.getLogger(LatencyAwarePolicy.class); - private static final boolean HOST_METRICS_ENABLED = - Boolean.getBoolean("com.datastax.driver.HOST_METRICS_ENABLED"); - - private final LoadBalancingPolicy childPolicy; - private final Tracker latencyTracker; - private final ScheduledExecutorService updaterService = - Executors.newSingleThreadScheduledExecutor(threadFactory("LatencyAwarePolicy updater")); - - private final double exclusionThreshold; - - private final long scale; - private final long retryPeriod; - private final long minMeasure; - private volatile Metrics metrics; - - private LatencyAwarePolicy( - LoadBalancingPolicy childPolicy, - double exclusionThreshold, - long scale, - long retryPeriod, - long updateRate, - int minMeasure) { - this.childPolicy = childPolicy; - this.retryPeriod = retryPeriod; - this.scale = scale; - this.latencyTracker = new Tracker(); - this.exclusionThreshold = exclusionThreshold; - this.minMeasure = minMeasure; - - updaterService.scheduleAtFixedRate(new Updater(), updateRate, updateRate, TimeUnit.NANOSECONDS); - } - - @Override - public LoadBalancingPolicy getChildPolicy() { - return childPolicy; - } - - /** - * Creates a new latency aware policy builder given the child policy that the resulting policy - * should wrap. - * - * @param childPolicy the load balancing policy to wrap with latency awareness. - * @return the created builder. - */ - public static Builder builder(LoadBalancingPolicy childPolicy) { - return new Builder(childPolicy); - } - - @VisibleForTesting - class Updater implements Runnable { - - private Set excludedAtLastTick = Collections.emptySet(); - - @Override - public void run() { - try { - logger.trace("Updating LatencyAwarePolicy minimum"); - latencyTracker.updateMin(); - - if (logger.isDebugEnabled()) { - /* - * For users to be able to know if the policy potentially needs tuning, we need to provide - * some feedback on on how things evolve. For that, we use the min computation to also check - * which host will be excluded if a query is submitted now and if any host is, we log it (but - * we try to avoid flooding too). This is probably interesting information anyway since it - * gets an idea of which host perform badly. - */ - Set excludedThisTick = new HashSet(); - double currentMin = latencyTracker.getMinAverage(); - for (Map.Entry entry : - getScoresSnapshot().getAllStats().entrySet()) { - Host host = entry.getKey(); - Snapshot.Stats stats = entry.getValue(); - if (stats.getMeasurementsCount() < minMeasure) continue; - - if (stats.lastUpdatedSince() > retryPeriod) { - if (excludedAtLastTick.contains(host)) - logger.debug( - String.format( - "Previously avoided host %s has not be queried since %.3fms: will be reconsidered.", - host, inMS(stats.lastUpdatedSince()))); - continue; - } - - if (stats.getLatencyScore() > ((long) (exclusionThreshold * currentMin))) { - excludedThisTick.add(host); - if (!excludedAtLastTick.contains(host)) - logger.debug( - String.format( - "Host %s has an average latency score of %.3fms, more than %f times more than the minimum %.3fms: will be avoided temporarily.", - host, inMS(stats.getLatencyScore()), exclusionThreshold, inMS(currentMin))); - continue; - } - - if (excludedAtLastTick.contains(host)) { - logger.debug( - "Previously avoided host {} average latency has come back within accepted bounds: will be reconsidered.", - host); - } - } - excludedAtLastTick = excludedThisTick; - } - } catch (RuntimeException e) { - // An unexpected exception would suppress further execution, so catch, log, but swallow - // after that. - logger.error("Error while updating LatencyAwarePolicy minimum", e); - } - } - } - - private static double inMS(long nanos) { - return ((double) nanos) / (1000 * 1000); - } - - private static double inMS(double nanos) { - return nanos / (1000 * 1000); - } - - private static ThreadFactory threadFactory(String nameFormat) { - return new ThreadFactoryBuilder().setNameFormat(nameFormat).build(); - } - - @Override - public void init(Cluster cluster, Collection hosts) { - childPolicy.init(cluster, hosts); - for (Host host : hosts) { - latencyTracker.addHost(host); - } - cluster.register(latencyTracker); - metrics = cluster.getMetrics(); - if (metrics != null) { - metrics - .getRegistry() - .register( - "LatencyAwarePolicy.latencies.min", - new Gauge() { - @Override - public Long getValue() { - return latencyTracker.getMinAverage(); - } - }); - } - } - - /** - * Returns the HostDistance for the provided host. - * - * @param host the host of which to return the distance of. - * @return the HostDistance to {@code host} as returned by the wrapped policy. - */ - @Override - public HostDistance distance(Host host) { - return childPolicy.distance(host); - } - - /** - * Returns the hosts to use for a new query. - * - *

The returned plan will be the same as the plan generated by the child policy, except that - * nodes that are slower than the best performing node by more than a configurable threshold will - * be moved to the end (that is, they will only be tried if all other nodes failed). Note that - * this policy only penalizes slow nodes, it does not globally sort the query plan by - * latency. - * - * @param loggedKeyspace the currently logged keyspace. - * @param statement the statement for which to build the plan. - * @return the new query plan. - */ - @Override - public Iterator newQueryPlan(String loggedKeyspace, Statement statement) { - final Iterator childIter = childPolicy.newQueryPlan(loggedKeyspace, statement); - return new AbstractIterator() { - - private Queue skipped; - - @Override - protected Host computeNext() { - long min = latencyTracker.getMinAverage(); - long now = System.nanoTime(); - while (childIter.hasNext()) { - Host host = childIter.next(); - TimestampedAverage latency = latencyTracker.latencyOf(host); - - // If we haven't had enough data point yet to have a score, or the last update of the - // score - // is just too old, include the host. - if (min < 0 - || latency == null - || latency.nbMeasure < minMeasure - || (now - latency.timestamp) > retryPeriod) { - if (hostMetricsEnabled()) { - metrics - .getRegistry() - .counter( - MetricsUtil.hostMetricName("LatencyAwarePolicy.inclusions-nodata.", host)) - .inc(); - } - return host; - } - - // If the host latency is within acceptable bound of the faster known host, return - // that host. Otherwise, skip it. - if (latency.average <= ((long) (exclusionThreshold * (double) min))) { - if (hostMetricsEnabled()) { - metrics - .getRegistry() - .counter(MetricsUtil.hostMetricName("LatencyAwarePolicy.inclusions.", host)) - .inc(); - } - return host; - } - - if (skipped == null) skipped = new ArrayDeque(); - skipped.offer(host); - if (hostMetricsEnabled()) { - metrics - .getRegistry() - .counter(MetricsUtil.hostMetricName("LatencyAwarePolicy.exclusions.", host)) - .inc(); - } - } - - if (skipped != null && !skipped.isEmpty()) { - Host host = skipped.poll(); - if (hostMetricsEnabled()) { - metrics - .getRegistry() - .counter( - MetricsUtil.hostMetricName("LatencyAwarePolicy.hits-while-excluded.", host)) - .inc(); - } - return host; - } - - return endOfData(); - }; - }; - } - - /** - * Returns a snapshot of the scores (latency averages) maintained by this policy. - * - * @return a new (immutable) {@link Snapshot} object containing the current latency scores - * maintained by this policy. - */ - public Snapshot getScoresSnapshot() { - Map currentLatencies = latencyTracker.currentLatencies(); - ImmutableMap.Builder builder = ImmutableMap.builder(); - long now = System.nanoTime(); - for (Map.Entry entry : currentLatencies.entrySet()) { - Host host = entry.getKey(); - TimestampedAverage latency = entry.getValue(); - Snapshot.Stats stats = - new Snapshot.Stats(now - latency.timestamp, latency.average, latency.nbMeasure); - builder.put(host, stats); - } - return new Snapshot(builder.build()); - } - - @Override - public void onUp(Host host) { - childPolicy.onUp(host); - latencyTracker.addHost(host); - } - - @Override - public void onDown(Host host) { - childPolicy.onDown(host); - latencyTracker.resetHost(host); - } - - @Override - public void onAdd(Host host) { - childPolicy.onAdd(host); - latencyTracker.addHost(host); - } - - @Override - public void onRemove(Host host) { - childPolicy.onRemove(host); - latencyTracker.resetHost(host); - } - - /** - * An immutable snapshot of the per-host scores (and stats in general) maintained by {@code - * LatencyAwarePolicy} to base its decision upon. - */ - public static class Snapshot { - private final Map stats; - - private Snapshot(Map stats) { - this.stats = stats; - } - - /** - * A map with the stats for all hosts tracked by the {@code LatencyAwarePolicy} at the time of - * the snapshot. - * - * @return a immutable map with all the stats contained in this snapshot. - */ - public Map getAllStats() { - return stats; - } - - /** - * The {@code Stats} object for a given host. - * - * @param host the host to return the stats of. - * @return the {@code Stats} for {@code host} in this snapshot or {@code null} if the snapshot - * has not information on {@code host}. - */ - public Stats getStats(Host host) { - return stats.get(host); - } - - /** A snapshot of the statistics on a given host kept by {@code LatencyAwarePolicy}. */ - public static class Stats { - private final long lastUpdatedSince; - private final long average; - private final long nbMeasurements; - - private Stats(long lastUpdatedSince, long average, long nbMeasurements) { - this.lastUpdatedSince = lastUpdatedSince; - this.average = average; - this.nbMeasurements = nbMeasurements; - } - - /** - * The number of nanoseconds since the last latency update was recorded (at the time of the - * snapshot). - * - * @return The number of nanoseconds since the last latency update was recorded (at the time - * of the snapshot). - */ - public long lastUpdatedSince() { - return lastUpdatedSince; - } - - /** - * The latency score for the host this is the stats of at the time of the snapshot. - * - * @return the latency score for the host this is the stats of at the time of the snapshot, or - * {@code -1L} if not enough measurements have been taken to assign a score. - */ - public long getLatencyScore() { - return average; - } - - /** - * The number of recorded latency measurements for the host this is the stats of. - * - * @return the number of recorded latency measurements for the host this is the stats of. - */ - public long getMeasurementsCount() { - return nbMeasurements; - } - } - } - - /** - * A set of DriverException subclasses that we should prevent from updating the host's score. The - * intent behind it is to filter out "fast" errors: when a host replies with such errors, it - * usually does so very quickly, because it did not involve any actual coordination work. Such - * errors are not good indicators of the host's responsiveness, and tend to make the host's score - * look better than it actually is. - */ - private static final Set> EXCLUDED_EXCEPTIONS = - ImmutableSet.of( - UnavailableException.class, // this is done via the snitch and is usually very fast - OverloadedException.class, - BootstrappingException.class, - UnpreparedException.class, - QueryValidationException - .class // query validation also happens at early stages in the coordinator - ); - - private class Tracker implements LatencyTracker { - - private final ConcurrentMap latencies = - new ConcurrentHashMap(); - private volatile long cachedMin = -1L; - - @Override - public void update( - final Host host, Statement statement, Exception exception, long newLatencyNanos) { - HostLatencyTracker hostTracker = latencies.get(host); - if (hostTracker != null) { - if (shouldConsiderNewLatency(statement, exception)) { - hostTracker.add(newLatencyNanos); - } else if (hostMetricsEnabled()) { - metrics - .getRegistry() - .counter(MetricsUtil.hostMetricName("LatencyAwarePolicy.ignored-latencies.", host)) - .inc(); - } - } - } - - private boolean shouldConsiderNewLatency(Statement statement, Exception exception) { - // query was successful: always consider - if (exception == null) return true; - // filter out "fast" errors - if (EXCLUDED_EXCEPTIONS.contains(exception.getClass())) return false; - return true; - } - - public void updateMin() { - long newMin = Long.MAX_VALUE; - long now = System.nanoTime(); - for (HostLatencyTracker tracker : latencies.values()) { - TimestampedAverage latency = tracker.getCurrentAverage(); - if (latency != null - && latency.average >= 0 - && latency.nbMeasure >= minMeasure - && (now - latency.timestamp) <= retryPeriod) newMin = Math.min(newMin, latency.average); - } - if (newMin != Long.MAX_VALUE) cachedMin = newMin; - } - - public long getMinAverage() { - return cachedMin; - } - - public TimestampedAverage latencyOf(Host host) { - HostLatencyTracker tracker = latencies.get(host); - return tracker == null ? null : tracker.getCurrentAverage(); - } - - public Map currentLatencies() { - Map map = new HashMap(latencies.size()); - for (Map.Entry entry : latencies.entrySet()) { - TimestampedAverage average = entry.getValue().getCurrentAverage(); - // average may be null if no latencies have been recorded yet for a host. - if (average != null) { - map.put(entry.getKey(), average); - } - } - return map; - } - - public void addHost(final Host host) { - logger.debug("Adding tracker for {}", host); - HostLatencyTracker old = - latencies.putIfAbsent(host, new HostLatencyTracker(scale, (30L * minMeasure) / 100L)); - if (old == null && hostMetricsEnabled()) { - String metricName = MetricsUtil.hostMetricName("LatencyAwarePolicy.latencies.", host); - if (!metrics.getRegistry().getNames().contains(metricName)) { - logger.debug("Adding gauge " + metricName); - metrics - .getRegistry() - .register( - metricName, - new Gauge() { - @Override - public Long getValue() { - TimestampedAverage latency = latencyTracker.latencyOf(host); - return (latency == null) ? -1 : latency.average; - } - }); - } - } - } - - public void resetHost(Host host) { - logger.debug("Removing tracker for {}", host); - latencies.remove(host); - } - - @Override - public void onRegister(Cluster cluster) { - // nothing to do - } - - @Override - public void onUnregister(Cluster cluster) { - // nothing to do - } - } - - private static class TimestampedAverage { - - private final long timestamp; - private final long average; - private final long nbMeasure; - - TimestampedAverage(long timestamp, long average, long nbMeasure) { - this.timestamp = timestamp; - this.average = average; - this.nbMeasure = nbMeasure; - } - } - - private static class HostLatencyTracker { - - private final long thresholdToAccount; - private final double scale; - private final AtomicReference current = - new AtomicReference(); - - HostLatencyTracker(long scale, long thresholdToAccount) { - this.scale = (double) scale; // We keep in double since that's how we'll use it. - this.thresholdToAccount = thresholdToAccount; - } - - public void add(long newLatencyNanos) { - TimestampedAverage previous, next; - do { - previous = current.get(); - next = computeNextAverage(previous, newLatencyNanos); - } while (next != null && !current.compareAndSet(previous, next)); - } - - private TimestampedAverage computeNextAverage( - TimestampedAverage previous, long newLatencyNanos) { - - long currentTimestamp = System.nanoTime(); - - long nbMeasure = previous == null ? 1 : previous.nbMeasure + 1; - if (nbMeasure < thresholdToAccount) - return new TimestampedAverage(currentTimestamp, -1L, nbMeasure); - - if (previous == null || previous.average < 0) - return new TimestampedAverage(currentTimestamp, newLatencyNanos, nbMeasure); - - // Note: it's possible for the delay to be 0, in which case newLatencyNanos will basically be - // discarded. It's fine: nanoTime is precise enough in practice that even if it happens, it - // will be very rare, and discarding a latency every once in a while is not the end of the - // world. - // We do test for negative value, even though in theory that should not happen, because it - // seems - // that historically there has been bugs here - // (https://blogs.oracle.com/dholmes/entry/inside_the_hotspot_vm_clocks) - // so while this is almost surely not a problem anymore, there's no reason to break the - // computation - // if this even happen. - long delay = currentTimestamp - previous.timestamp; - if (delay <= 0) return null; - - double scaledDelay = ((double) delay) / scale; - // Note: We don't use log1p because we it's quite a bit slower and we don't care about the - // precision (and since we - // refuse ridiculously big scales, scaledDelay can't be so low that scaledDelay+1 == 1.0 (due - // to rounding)). - double prevWeight = Math.log(scaledDelay + 1) / scaledDelay; - long newAverage = - (long) ((1.0 - prevWeight) * newLatencyNanos + prevWeight * previous.average); - - return new TimestampedAverage(currentTimestamp, newAverage, nbMeasure); - } - - public TimestampedAverage getCurrentAverage() { - return current.get(); - } - } - - /** - * Helper builder object to create a latency aware policy. - * - *

This helper allows to configure the different parameters used by {@code LatencyAwarePolicy}. - * The only mandatory parameter is the child policy that will be wrapped with latency awareness. - * The other parameters can be set through the methods of this builder, but all have defaults - * (that are documented in the javadoc of each method) if you don't. - * - *

If you observe that the resulting policy excludes hosts too aggressively or not enough so, - * the main parameters to check are the exclusion threshold ({@link #withExclusionThreshold}) and - * scale ({@link #withScale}). - * - * @since 1.0.4 - */ - public static class Builder { - - public static final double DEFAULT_EXCLUSION_THRESHOLD = 2.0; - public static final long DEFAULT_SCALE_NANOS = TimeUnit.MILLISECONDS.toNanos(100); - public static final long DEFAULT_RETRY_PERIOD_NANOS = TimeUnit.SECONDS.toNanos(10); - public static final long DEFAULT_UPDATE_RATE_NANOS = TimeUnit.MILLISECONDS.toNanos(100); - public static final int DEFAULT_MIN_MEASURE = 50; - - private final LoadBalancingPolicy childPolicy; - - private double exclusionThreshold = DEFAULT_EXCLUSION_THRESHOLD; - private long scale = DEFAULT_SCALE_NANOS; - private long retryPeriod = DEFAULT_RETRY_PERIOD_NANOS; - private long updateRate = DEFAULT_UPDATE_RATE_NANOS; - private int minMeasure = DEFAULT_MIN_MEASURE; - - /** - * Creates a new latency aware policy builder given the child policy that the resulting policy - * wraps. - * - * @param childPolicy the load balancing policy to wrap with latency awareness. - */ - public Builder(LoadBalancingPolicy childPolicy) { - this.childPolicy = childPolicy; - } - - /** - * Sets the exclusion threshold to use for the resulting latency aware policy. - * - *

The exclusion threshold controls how much worse the average latency of a node must be - * compared to the fastest performing node for it to be penalized by the policy. - * - *

The default exclusion threshold (if this method is not called) is 2. In other - * words, the resulting policy excludes nodes that are more than twice slower than the fastest - * node. - * - * @param exclusionThreshold the exclusion threshold to use. Must be greater or equal to 1. - * @return this builder. - * @throws IllegalArgumentException if {@code exclusionThreshold < 1}. - */ - public Builder withExclusionThreshold(double exclusionThreshold) { - if (exclusionThreshold < 1d) - throw new IllegalArgumentException("Invalid exclusion threshold, must be greater than 1."); - this.exclusionThreshold = exclusionThreshold; - return this; - } - - /** - * Sets the scale to use for the resulting latency aware policy. - * - *

The {@code scale} provides control on how the weight given to older latencies decreases - * over time. For a given host, if a new latency {@code l} is received at time {@code t}, and - * the previously calculated average is {@code prev} calculated at time {@code t'}, then the - * newly calculated average {@code avg} for that host is calculated thusly: - * - *

{@code d = (t - t') / scale
-     * alpha = 1 - (ln(d+1) / d)
-     * avg = alpha * l + (1 - alpha) * prev}
- * - * Typically, with a {@code scale} of 100 milliseconds (the default), if a new latency is - * measured and the previous measure is 10 millisecond old (so {@code d=0.1}), then {@code - * alpha} will be around {@code 0.05}. In other words, the new latency will weight 5% of the - * updated average. A bigger scale will get less weight to new measurements (compared to - * previous ones), a smaller one will give them more weight. - * - *

The default scale (if this method is not used) is of 100 milliseconds. If unsure, - * try this default scale first and experiment only if it doesn't provide acceptable results - * (hosts are excluded too quickly or not fast enough and tuning the exclusion threshold doesn't - * help). - * - * @param scale the scale to use. - * @param unit the unit of {@code scale}. - * @return this builder. - * @throws IllegalArgumentException if {@code scale <= 0}. - */ - public Builder withScale(long scale, TimeUnit unit) { - if (scale <= 0) - throw new IllegalArgumentException("Invalid scale, must be strictly positive"); - this.scale = unit.toNanos(scale); - return this; - } - - /** - * Sets the retry period for the resulting latency aware policy. - * - *

The retry period defines how long a node may be penalized by the policy before it is given - * a 2nd change. More precisely, a node is excluded from query plans if both his calculated - * average latency is {@code exclusionThreshold} times slower than the fastest node average - * latency (at the time the query plan is computed) and his calculated average latency - * has been updated since less than {@code retryPeriod}. Since penalized nodes will likely not - * see their latency updated, this is basically how long the policy will exclude a node. - * - * @param retryPeriod the retry period to use. - * @param unit the unit for {@code retryPeriod}. - * @return this builder. - * @throws IllegalArgumentException if {@code retryPeriod < 0}. - */ - public Builder withRetryPeriod(long retryPeriod, TimeUnit unit) { - if (retryPeriod < 0) - throw new IllegalArgumentException("Invalid retry period, must be positive"); - this.retryPeriod = unit.toNanos(retryPeriod); - return this; - } - - /** - * Sets the update rate for the resulting latency aware policy. - * - *

The update rate defines how often the minimum average latency is recomputed. While the - * average latency score of each node is computed iteratively (updated each time a new latency - * is collected), the minimum score needs to be recomputed from scratch every time, which is - * slightly more costly. For this reason, the minimum is only re-calculated at the given fixed - * rate and cached between re-calculation. - * - *

The default update rate if 100 milliseconds, which should be appropriate for most - * applications. In particular, note that while we want to avoid to recompute the minimum for - * every query, that computation is not particularly intensive either and there is no reason to - * use a very slow rate (more than second is probably unnecessarily slow for instance). - * - * @param updateRate the update rate to use. - * @param unit the unit for {@code updateRate}. - * @return this builder. - * @throws IllegalArgumentException if {@code updateRate <e; 0}. - */ - public Builder withUpdateRate(long updateRate, TimeUnit unit) { - if (updateRate <= 0) - throw new IllegalArgumentException("Invalid update rate value, must be strictly positive"); - this.updateRate = unit.toNanos(updateRate); - return this; - } - - /** - * Sets the minimum number of measurements per-host to consider for the resulting latency aware - * policy. - * - *

Penalizing nodes is based on an average of their recently measured average latency. This - * average is only meaningful if a minimum of measurements have been collected (moreover, a - * newly started Cassandra node will tend to perform relatively poorly on the first queries due - * to the JVM warmup). This is what this option controls. If less that {@code minMeasure} data - * points have been collected for a given host, the policy will never penalize that host. Also, - * the 30% first measurement will be entirely ignored (in other words, the {@code 30% * - * minMeasure} first measurement to a node are entirely ignored, while the {@code 70%} next ones - * are accounted in the latency computed but the node won't get convicted until we've had at - * least {@code minMeasure} measurements). - * - *

Note that the number of collected measurements for a given host is reset if the node is - * restarted. - * - *

The default for this option (if this method is not called) is 50. Note that it is - * probably not a good idea to put this option too low if only to avoid the influence of JVM - * warm-up on newly restarted nodes. - * - * @param minMeasure the minimum measurements to consider. - * @return this builder. - * @throws IllegalArgumentException if {@code minMeasure < 0}. - */ - public Builder withMininumMeasurements(int minMeasure) { - if (minMeasure < 0) - throw new IllegalArgumentException("Invalid minimum measurements value, must be positive"); - this.minMeasure = minMeasure; - return this; - } - - /** - * Builds a new latency aware policy using the options set on this builder. - * - * @return the newly created {@code LatencyAwarePolicy}. - */ - public LatencyAwarePolicy build() { - return new LatencyAwarePolicy( - childPolicy, exclusionThreshold, scale, retryPeriod, updateRate, minMeasure); - } - } - - @Override - public void close() { - childPolicy.close(); - updaterService.shutdown(); - } - - private boolean hostMetricsEnabled() { - return HOST_METRICS_ENABLED && metrics != null; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/LoadBalancingPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/LoadBalancingPolicy.java deleted file mode 100644 index 27684060ea3..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/LoadBalancingPolicy.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.HostDistance; -import com.datastax.driver.core.Statement; -import java.util.Collection; -import java.util.Iterator; - -/** - * The policy that decides which Cassandra hosts to contact for each new query. - * - *

Two methods need to be implemented: - * - *

    - *
  • {@link LoadBalancingPolicy#distance}: returns the "distance" of an host for that balancing - * policy. - *
  • {@link LoadBalancingPolicy#newQueryPlan}: it is used for each query to find which host to - * query first, and which hosts to use as failover. - *
- * - *

The {@code LoadBalancingPolicy} is informed of hosts up/down events. For efficiency purposes, - * the policy is expected to exclude down hosts from query plans. - */ -public interface LoadBalancingPolicy { - - /** - * Initialize this load balancing policy. - * - *

Note that the driver guarantees that it will call this method exactly once per policy object - * and will do so before any call to another of the methods of the policy. - * - * @param cluster the {@code Cluster} instance for which the policy is created. - * @param hosts the initial hosts to use. - */ - public void init(Cluster cluster, Collection hosts); - - /** - * Returns the distance assigned by this policy to the provided host. - * - *

The distance of an host influence how much connections are kept to the node (see {@link - * HostDistance}). A policy should assign a {@code LOCAL} distance to nodes that are susceptible - * to be returned first by {@code newQueryPlan} and it is useless for {@code newQueryPlan} to - * return hosts to which it assigns an {@code IGNORED} distance. - * - *

The host distance is primarily used to prevent keeping too many connections to host in - * remote datacenters when the policy itself always picks host in the local datacenter first. - * - * @param host the host of which to return the distance of. - * @return the HostDistance to {@code host}. - */ - public HostDistance distance(Host host); - - /** - * Returns the hosts to use for a new query. - * - *

Each new query will call this method. The first host in the result will then be used to - * perform the query. In the event of a connection problem (the queried host is down or appear to - * be so), the next host will be used. If all hosts of the returned {@code Iterator} are down, the - * query will fail. - * - * @param loggedKeyspace the currently logged keyspace (the one set through either {@link - * Cluster#connect(String)} or by manually doing a {@code USE} query) for the session on which - * this plan need to be built. This can be {@code null} if the corresponding session has no - * keyspace logged in. - * @param statement the query for which to build a plan. - * @return an iterator of Host. The query is tried against the hosts returned by this iterator in - * order, until the query has been sent successfully to one of the host. - */ - public Iterator newQueryPlan(String loggedKeyspace, Statement statement); - - /** - * Called when a new node is added to the cluster. - * - *

The newly added node should be considered up. - * - * @param host the host that has been newly added. - */ - void onAdd(Host host); - - /** - * Called when a node is determined to be up. - * - * @param host the host that has been detected up. - */ - void onUp(Host host); - - /** - * Called when a node is determined to be down. - * - * @param host the host that has been detected down. - */ - void onDown(Host host); - - /** - * Called when a node is removed from the cluster. - * - * @param host the removed host. - */ - void onRemove(Host host); - - /** - * Gets invoked at cluster shutdown. - * - *

This gives the policy the opportunity to perform some cleanup, for instance stop threads - * that it might have started. - */ - void close(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/LoggingRetryPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/LoggingRetryPolicy.java deleted file mode 100644 index 791b2b5da79..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/LoggingRetryPolicy.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.WriteType; -import com.datastax.driver.core.exceptions.DriverException; -import com.google.common.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A retry policy that wraps another policy, logging the decision made by its sub-policy. - * - *

Note that this policy only logs {@link - * com.datastax.driver.core.policies.RetryPolicy.RetryDecision.Type#RETRY RETRY} and {@link - * com.datastax.driver.core.policies.RetryPolicy.RetryDecision.Type#IGNORE IGNORE} decisions (since - * {@link com.datastax.driver.core.policies.RetryPolicy.RetryDecision.Type#RETHROW RETHROW} - * decisions are just meant to propagate the Cassandra exception). - * - *

The logging is done at the INFO level and the logger name is {@code - * com.datastax.driver.core.policies.LoggingRetryPolicy}. - */ -public class LoggingRetryPolicy implements RetryPolicy { - - private static final Logger logger = LoggerFactory.getLogger(LoggingRetryPolicy.class); - - @VisibleForTesting - static final String IGNORING_READ_TIMEOUT = - "Ignoring read timeout (initial consistency: {}, required responses: {}, received responses: {}, data retrieved: {}, retries: {})"; - - @VisibleForTesting - static final String RETRYING_ON_READ_TIMEOUT = - "Retrying on read timeout on {} at consistency {} (initial consistency: {}, required responses: {}, received responses: {}, data retrieved: {}, retries: {})"; - - @VisibleForTesting - static final String IGNORING_WRITE_TIMEOUT = - "Ignoring write timeout (initial consistency: {}, write type: {}, required acknowledgments: {}, received acknowledgments: {}, retries: {})"; - - @VisibleForTesting - static final String RETRYING_ON_WRITE_TIMEOUT = - "Retrying on write timeout on {} at consistency {} (initial consistency: {}, write type: {}, required acknowledgments: {}, received acknowledgments: {}, retries: {})"; - - @VisibleForTesting - static final String IGNORING_UNAVAILABLE = - "Ignoring unavailable exception (initial consistency: {}, required replica: {}, alive replica: {}, retries: {})"; - - @VisibleForTesting - static final String RETRYING_ON_UNAVAILABLE = - "Retrying on unavailable exception on {} at consistency {} (initial consistency: {}, required replica: {}, alive replica: {}, retries: {})"; - - @VisibleForTesting - static final String IGNORING_REQUEST_ERROR = - "Ignoring request error (initial consistency: {}, retries: {}, exception: {})"; - - @VisibleForTesting - static final String RETRYING_ON_REQUEST_ERROR = - "Retrying on request error on {} at consistency {} (initial consistency: {}, retries: {}, exception: {})"; - - private final RetryPolicy policy; - - /** - * Creates a new {@code RetryPolicy} that logs the decision of {@code policy}. - * - * @param policy the policy to wrap. The policy created by this constructor will return the same - * decision than {@code policy} but will log them. - */ - public LoggingRetryPolicy(RetryPolicy policy) { - this.policy = policy; - } - - private static ConsistencyLevel cl(ConsistencyLevel cl, RetryDecision decision) { - return decision.getRetryConsistencyLevel() == null ? cl : decision.getRetryConsistencyLevel(); - } - - private static String host(RetryDecision decision) { - return decision.isRetryCurrent() ? "same host" : "next host"; - } - - @Override - public RetryDecision onReadTimeout( - Statement statement, - ConsistencyLevel cl, - int requiredResponses, - int receivedResponses, - boolean dataRetrieved, - int nbRetry) { - RetryDecision decision = - policy.onReadTimeout( - statement, cl, requiredResponses, receivedResponses, dataRetrieved, nbRetry); - switch (decision.getType()) { - case IGNORE: - logDecision( - IGNORING_READ_TIMEOUT, - cl, - requiredResponses, - receivedResponses, - dataRetrieved, - nbRetry); - break; - case RETRY: - logDecision( - RETRYING_ON_READ_TIMEOUT, - host(decision), - cl(cl, decision), - cl, - requiredResponses, - receivedResponses, - dataRetrieved, - nbRetry); - break; - } - return decision; - } - - @Override - public RetryDecision onWriteTimeout( - Statement statement, - ConsistencyLevel cl, - WriteType writeType, - int requiredAcks, - int receivedAcks, - int nbRetry) { - RetryDecision decision = - policy.onWriteTimeout(statement, cl, writeType, requiredAcks, receivedAcks, nbRetry); - switch (decision.getType()) { - case IGNORE: - logDecision(IGNORING_WRITE_TIMEOUT, cl, writeType, requiredAcks, receivedAcks, nbRetry); - break; - case RETRY: - logDecision( - RETRYING_ON_WRITE_TIMEOUT, - host(decision), - cl(cl, decision), - cl, - writeType, - requiredAcks, - receivedAcks, - nbRetry); - break; - } - return decision; - } - - @Override - public RetryDecision onUnavailable( - Statement statement, - ConsistencyLevel cl, - int requiredReplica, - int aliveReplica, - int nbRetry) { - RetryDecision decision = - policy.onUnavailable(statement, cl, requiredReplica, aliveReplica, nbRetry); - switch (decision.getType()) { - case IGNORE: - logDecision(IGNORING_UNAVAILABLE, cl, requiredReplica, aliveReplica, nbRetry); - break; - case RETRY: - logDecision( - RETRYING_ON_UNAVAILABLE, - host(decision), - cl(cl, decision), - cl, - requiredReplica, - aliveReplica, - nbRetry); - break; - } - return decision; - } - - @Override - public RetryDecision onRequestError( - Statement statement, ConsistencyLevel cl, DriverException e, int nbRetry) { - RetryDecision decision = policy.onRequestError(statement, cl, e, nbRetry); - switch (decision.getType()) { - case IGNORE: - logDecision(IGNORING_REQUEST_ERROR, cl, nbRetry, e.toString()); - break; - case RETRY: - logDecision( - RETRYING_ON_REQUEST_ERROR, host(decision), cl(cl, decision), cl, nbRetry, e.toString()); - break; - } - return decision; - } - - @Override - public void init(Cluster cluster) { - policy.init(cluster); - } - - @Override - public void close() { - policy.close(); - } - - /** - * Logs the decision according to the given template and parameters. The log level is INFO, but - * subclasses may override. - * - * @param template The template to use; arguments must be specified in SLF4J style, i.e. {@code - * "{}"}. - * @param parameters The template parameters. - */ - protected void logDecision(String template, Object... parameters) { - logger.info(template, parameters); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/NoSpeculativeExecutionPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/NoSpeculativeExecutionPolicy.java deleted file mode 100644 index 587349545b9..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/NoSpeculativeExecutionPolicy.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.Statement; - -/** A {@link SpeculativeExecutionPolicy} that never schedules speculative executions. */ -public class NoSpeculativeExecutionPolicy implements SpeculativeExecutionPolicy { - - /** The single instance (this class is stateless). */ - public static final NoSpeculativeExecutionPolicy INSTANCE = new NoSpeculativeExecutionPolicy(); - - private static final SpeculativeExecutionPlan PLAN = - new SpeculativeExecutionPlan() { - @Override - public long nextExecution(Host lastQueried) { - return -1; - } - }; - - @Override - public SpeculativeExecutionPlan newPlan(String loggedKeyspace, Statement statement) { - return PLAN; - } - - private NoSpeculativeExecutionPolicy() { - // do nothing - } - - @Override - public void init(Cluster cluster) { - // do nothing - } - - @Override - public void close() { - // do nothing - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/PercentileSpeculativeExecutionPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/PercentileSpeculativeExecutionPolicy.java deleted file mode 100644 index ecad11826c9..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/PercentileSpeculativeExecutionPolicy.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import static com.google.common.base.Preconditions.checkArgument; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.LatencyTracker; -import com.datastax.driver.core.PercentileTracker; -import com.datastax.driver.core.Statement; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * A policy that triggers speculative executions when the request to the current host is above a - * given percentile. - */ -public class PercentileSpeculativeExecutionPolicy implements SpeculativeExecutionPolicy { - private final PercentileTracker percentileTracker; - private final double percentile; - private final int maxSpeculativeExecutions; - - /** - * Builds a new instance. - * - * @param percentileTracker the component that will record latencies. It will get {@link - * Cluster#register(LatencyTracker) registered} with the cluster when this policy initializes. - * @param percentile the percentile that a request's latency must fall into to be considered slow - * (ex: {@code 99.0}). - * @param maxSpeculativeExecutions the maximum number of speculative executions that will be - * triggered for a given request (this does not include the initial, normal request). Must be - * strictly positive. - */ - public PercentileSpeculativeExecutionPolicy( - PercentileTracker percentileTracker, double percentile, int maxSpeculativeExecutions) { - checkArgument( - maxSpeculativeExecutions > 0, - "number of speculative executions must be strictly positive (was %d)", - maxSpeculativeExecutions); - checkArgument( - percentile >= 0.0 && percentile < 100, "percentile must be between 0.0 and 100 (was %f)"); - - this.percentileTracker = percentileTracker; - this.percentile = percentile; - this.maxSpeculativeExecutions = maxSpeculativeExecutions; - } - - @Override - public SpeculativeExecutionPlan newPlan(String loggedKeyspace, Statement statement) { - return new SpeculativeExecutionPlan() { - private final AtomicInteger remaining = new AtomicInteger(maxSpeculativeExecutions); - - @Override - public long nextExecution(Host lastQueried) { - if (remaining.getAndDecrement() > 0) - return percentileTracker.getLatencyAtPercentile(lastQueried, null, null, percentile); - else return -1; - } - }; - } - - @Override - public void init(Cluster cluster) { - cluster.register(percentileTracker); - } - - @Override - public void close() { - // nothing - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/Policies.java b/driver-core/src/main/java/com/datastax/driver/core/policies/Policies.java deleted file mode 100644 index f74d579c961..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/Policies.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.AtomicMonotonicTimestampGenerator; -import com.datastax.driver.core.DefaultEndPointFactory; -import com.datastax.driver.core.EndPointFactory; -import com.datastax.driver.core.TimestampGenerator; - -/** Policies configured for a {@link com.datastax.driver.core.Cluster} instance. */ -public class Policies { - - /** - * Returns a builder to create a new {@code Policies} object. - * - * @return the builder. - */ - public static Builder builder() { - return new Builder(); - } - - private static final ReconnectionPolicy DEFAULT_RECONNECTION_POLICY = - new ExponentialReconnectionPolicy(1000, 10 * 60 * 1000); - private static final RetryPolicy DEFAULT_RETRY_POLICY = DefaultRetryPolicy.INSTANCE; - private static final AddressTranslator DEFAULT_ADDRESS_TRANSLATOR = new IdentityTranslator(); - private static final SpeculativeExecutionPolicy DEFAULT_SPECULATIVE_EXECUTION_POLICY = - NoSpeculativeExecutionPolicy.INSTANCE; - - private final LoadBalancingPolicy loadBalancingPolicy; - private final ReconnectionPolicy reconnectionPolicy; - private final RetryPolicy retryPolicy; - private final AddressTranslator addressTranslator; - private final TimestampGenerator timestampGenerator; - private final SpeculativeExecutionPolicy speculativeExecutionPolicy; - private final EndPointFactory endPointFactory; - - private Policies( - LoadBalancingPolicy loadBalancingPolicy, - ReconnectionPolicy reconnectionPolicy, - RetryPolicy retryPolicy, - AddressTranslator addressTranslator, - TimestampGenerator timestampGenerator, - SpeculativeExecutionPolicy speculativeExecutionPolicy, - EndPointFactory endPointFactory) { - this.loadBalancingPolicy = loadBalancingPolicy; - this.reconnectionPolicy = reconnectionPolicy; - this.retryPolicy = retryPolicy; - this.addressTranslator = addressTranslator; - this.timestampGenerator = timestampGenerator; - this.speculativeExecutionPolicy = speculativeExecutionPolicy; - this.endPointFactory = endPointFactory; - } - - /** - * The default load balancing policy. - * - *

The default load balancing policy is {@link DCAwareRoundRobinPolicy} with token awareness - * (so {@code new TokenAwarePolicy(new DCAwareRoundRobinPolicy())}). - * - *

Note that this policy shuffles the replicas when token awareness is used, see {@link - * TokenAwarePolicy#TokenAwarePolicy(LoadBalancingPolicy, boolean)} for an explanation of the - * tradeoffs. - * - * @return the default load balancing policy. - */ - public static LoadBalancingPolicy defaultLoadBalancingPolicy() { - // Note: balancing policies are stateful, so we can't store that in a static or that would screw - // thing - // up if multiple Cluster instance are started in the same JVM. - return new TokenAwarePolicy(DCAwareRoundRobinPolicy.builder().build()); - } - - /** - * The default reconnection policy. - * - *

The default reconnection policy is an {@link ExponentialReconnectionPolicy} where the base - * delay is 1 second and the max delay is 10 minutes; - * - * @return the default reconnection policy. - */ - public static ReconnectionPolicy defaultReconnectionPolicy() { - return DEFAULT_RECONNECTION_POLICY; - } - - /** - * The default retry policy. - * - *

The default retry policy is {@link DefaultRetryPolicy}. - * - * @return the default retry policy. - */ - public static RetryPolicy defaultRetryPolicy() { - return DEFAULT_RETRY_POLICY; - } - - /** - * The default address translator. - * - *

The default address translator is {@link IdentityTranslator}. - * - * @return the default address translator. - */ - public static AddressTranslator defaultAddressTranslator() { - return DEFAULT_ADDRESS_TRANSLATOR; - } - - /** - * The default timestamp generator. - * - *

This is an instance of {@link AtomicMonotonicTimestampGenerator}. - * - * @return the default timestamp generator. - */ - public static TimestampGenerator defaultTimestampGenerator() { - return new AtomicMonotonicTimestampGenerator(); - } - - /** - * The default speculative retry policy. - * - *

The default speculative retry policy is a {@link NoSpeculativeExecutionPolicy}. - * - * @return the default speculative retry policy. - */ - public static SpeculativeExecutionPolicy defaultSpeculativeExecutionPolicy() { - return DEFAULT_SPECULATIVE_EXECUTION_POLICY; - } - - public static EndPointFactory defaultEndPointFactory() { - return new DefaultEndPointFactory(); - } - - /** - * The load balancing policy in use. - * - *

The load balancing policy defines how Cassandra hosts are picked for queries. - * - * @return the load balancing policy in use. - */ - public LoadBalancingPolicy getLoadBalancingPolicy() { - return loadBalancingPolicy; - } - - /** - * The reconnection policy in use. - * - *

The reconnection policy defines how often the driver tries to reconnect to a dead node. - * - * @return the reconnection policy in use. - */ - public ReconnectionPolicy getReconnectionPolicy() { - return reconnectionPolicy; - } - - /** - * The retry policy in use. - * - *

The retry policy defines in which conditions a query should be automatically retries by the - * driver. - * - * @return the retry policy in use. - */ - public RetryPolicy getRetryPolicy() { - return retryPolicy; - } - - /** - * The address translator in use. - * - * @return the address translator in use. - */ - public AddressTranslator getAddressTranslator() { - return addressTranslator; - } - - /** - * The timestamp generator to use. - * - * @return the timestamp generator to use. - */ - public TimestampGenerator getTimestampGenerator() { - return timestampGenerator; - } - - /** - * The speculative execution policy in use. - * - * @return the speculative execution policy in use. - */ - public SpeculativeExecutionPolicy getSpeculativeExecutionPolicy() { - return speculativeExecutionPolicy; - } - - public EndPointFactory getEndPointFactory() { - return endPointFactory; - } - - /** A builder to create a new {@code Policies} object. */ - public static class Builder { - private LoadBalancingPolicy loadBalancingPolicy; - private ReconnectionPolicy reconnectionPolicy; - private RetryPolicy retryPolicy; - private AddressTranslator addressTranslator; - private TimestampGenerator timestampGenerator; - private SpeculativeExecutionPolicy speculativeExecutionPolicy; - private EndPointFactory endPointFactory; - - /** - * Sets the load balancing policy. - * - * @param loadBalancingPolicy see {@link #getLoadBalancingPolicy()}. - * @return this builder. - */ - public Builder withLoadBalancingPolicy(LoadBalancingPolicy loadBalancingPolicy) { - this.loadBalancingPolicy = loadBalancingPolicy; - return this; - } - - /** - * Sets the reconnection policy. - * - * @param reconnectionPolicy see {@link #getReconnectionPolicy()}. - * @return this builder. - */ - public Builder withReconnectionPolicy(ReconnectionPolicy reconnectionPolicy) { - this.reconnectionPolicy = reconnectionPolicy; - return this; - } - - /** - * Sets the retry policy. - * - * @param retryPolicy see {@link #getRetryPolicy()}. - * @return this builder. - */ - public Builder withRetryPolicy(RetryPolicy retryPolicy) { - this.retryPolicy = retryPolicy; - return this; - } - - /** - * Sets the address translator. - * - * @param addressTranslator see {@link #getAddressTranslator()}. - * @return this builder. - */ - public Builder withAddressTranslator(AddressTranslator addressTranslator) { - this.addressTranslator = addressTranslator; - return this; - } - - /** - * Sets the timestamp generator. - * - * @param timestampGenerator see {@link #getTimestampGenerator()}. - * @return this builder. - */ - public Builder withTimestampGenerator(TimestampGenerator timestampGenerator) { - this.timestampGenerator = timestampGenerator; - return this; - } - - /** - * Sets the speculative execution policy. - * - * @param speculativeExecutionPolicy see {@link #getSpeculativeExecutionPolicy()}. - * @return this builder. - */ - public Builder withSpeculativeExecutionPolicy( - SpeculativeExecutionPolicy speculativeExecutionPolicy) { - this.speculativeExecutionPolicy = speculativeExecutionPolicy; - return this; - } - - public Builder withEndPointFactory(EndPointFactory endPointFactory) { - this.endPointFactory = endPointFactory; - return this; - } - - /** - * Builds the final object from this builder. - * - *

Any field that hasn't been set explicitly will get its default value. - * - * @return the object. - */ - public Policies build() { - return new Policies( - loadBalancingPolicy == null ? defaultLoadBalancingPolicy() : loadBalancingPolicy, - reconnectionPolicy == null ? defaultReconnectionPolicy() : reconnectionPolicy, - retryPolicy == null ? defaultRetryPolicy() : retryPolicy, - addressTranslator == null ? defaultAddressTranslator() : addressTranslator, - timestampGenerator == null ? defaultTimestampGenerator() : timestampGenerator, - speculativeExecutionPolicy == null - ? defaultSpeculativeExecutionPolicy() - : speculativeExecutionPolicy, - endPointFactory == null ? defaultEndPointFactory() : endPointFactory); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/ReconnectionPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/ReconnectionPolicy.java deleted file mode 100644 index d51d687ecca..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/ReconnectionPolicy.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; - -/** - * Policy that decides how often the reconnection to a dead node is attempted. - * - *

Each time a node is detected dead (because a connection error occurs), a new {@code - * ReconnectionSchedule} instance is created (through the {@link #newSchedule()}). Then each call to - * the {@link ReconnectionSchedule#nextDelayMs} method of this instance will decide when the next - * reconnection attempt to this node will be tried. - * - *

Note that if the driver receives a push notification from the Cassandra cluster that a node is - * UP, any existing {@code ReconnectionSchedule} on that node will be cancelled and a new one will - * be created (in effect, the driver reset the scheduler). - * - *

The default {@link ExponentialReconnectionPolicy} policy is usually adequate. - */ -public interface ReconnectionPolicy { - - /** - * Creates a new schedule for reconnection attempts. - * - * @return the created schedule. - */ - public ReconnectionSchedule newSchedule(); - - /** Schedules reconnection attempts to a node. */ - public interface ReconnectionSchedule { - - /** - * When to attempt the next reconnection. - * - *

This method will be called once when the host is detected down to schedule the first - * reconnection attempt, and then once after each failed reconnection attempt to schedule the - * next one. Hence each call to this method are free to return a different value. - * - * @return a time in milliseconds to wait before attempting the next reconnection. - */ - public long nextDelayMs(); - } - - /** - * Gets invoked at cluster startup. - * - * @param cluster the cluster that this policy is associated with. - */ - void init(Cluster cluster); - - /** - * Gets invoked at cluster shutdown. - * - *

This gives the policy the opportunity to perform some cleanup, for instance stop threads - * that it might have started. - */ - void close(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/RetryPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/RetryPolicy.java deleted file mode 100644 index 8be904d2778..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/RetryPolicy.java +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.SocketOptions; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.WriteType; -import com.datastax.driver.core.exceptions.DriverException; - -/** - * A policy that defines a default behavior to adopt when a request fails. - * - *

Such policy allows to centralize the handling of query retries, allowing to minimize the need - * for exception catching/handling in business code. - */ -public interface RetryPolicy { - - /** - * A retry decision to adopt on a Cassandra exception (read/write timeout or unavailable - * exception). - * - *

There are three possible decisions: - * - *

    - *
  • RETHROW: no retry should be attempted and an exception should be thrown. - *
  • RETRY: the operation will be retried. The consistency level of the retry should be - * specified. - *
  • IGNORE: no retry should be attempted and the exception should be ignored. In that case, - * the operation that triggered the Cassandra exception will return an empty result set. - *
- */ - class RetryDecision { - - private static final RetryDecision RETHROW_DECISION = - new RetryDecision(Type.RETHROW, null, true); - private static final RetryDecision IGNORE_DECISION = new RetryDecision(Type.IGNORE, null, true); - - /** The types of retry decisions. */ - public enum Type { - RETRY, - RETHROW, - IGNORE - } - - private final Type type; - private final ConsistencyLevel retryCL; - private final boolean retryCurrent; - - private RetryDecision(Type type, ConsistencyLevel retryCL, boolean retryCurrent) { - this.type = type; - this.retryCL = retryCL; - this.retryCurrent = retryCurrent; - } - - /** - * The type of this retry decision. - * - * @return the type of this retry decision. - */ - public Type getType() { - return type; - } - - /** - * The consistency level for this retry decision. This is only meaningful for {@code RETRY} - * decisions. The consistency level is always {@code null} for an {@code IGNORE} or a {@code - * RETHROW} decision; for a {@code RETRY} decision, the consistency level can be {@code null}, - * in which case the retry is done at the same consistency level as in the previous attempt. - * - * @return the consistency level for a retry decision. - */ - public ConsistencyLevel getRetryConsistencyLevel() { - return retryCL; - } - - /** - * Whether this decision is to retry the same host. This is only meaningful for {@code RETRY} - * decisions. - * - * @return {@code true} if the decision is to retry the same host, {@code false} otherwise. - * Default is {@code false}. - */ - public boolean isRetryCurrent() { - return retryCurrent; - } - - /** - * Creates a {@link RetryDecision.Type#RETHROW} retry decision. - * - * @return a {@link RetryDecision.Type#RETHROW} retry decision. - */ - public static RetryDecision rethrow() { - return RETHROW_DECISION; - } - - /** - * Creates a {@link RetryDecision.Type#RETRY} retry decision using the same host and the - * provided consistency level. - * - *

If the provided consistency level is {@code null}, the retry will be done at the same - * consistency level as the previous attempt. - * - *

Beware that {@link ConsistencyLevel#isSerial() serial} consistency levels should never be - * passed to this method; attempting to do so would trigger an {@link - * com.datastax.driver.core.exceptions.InvalidQueryException InvalidQueryException}. - * - * @param consistency the consistency level to use for the retry; if {@code null}, the same - * level as the previous attempt will be used. - * @return a {@link RetryDecision.Type#RETRY} decision using the same host and the provided - * consistency level - */ - public static RetryDecision retry(ConsistencyLevel consistency) { - return new RetryDecision(Type.RETRY, consistency, true); - } - - /** - * Creates an {@link RetryDecision.Type#IGNORE} retry decision. - * - * @return an {@link RetryDecision.Type#IGNORE} retry decision. - */ - public static RetryDecision ignore() { - return IGNORE_DECISION; - } - - /** - * Creates a {@link RetryDecision.Type#RETRY} retry decision using the next host in the query - * plan, and using the provided consistency level. - * - *

If the provided consistency level is {@code null}, the retry will be done at the same - * consistency level as the previous attempt. - * - *

Beware that {@link ConsistencyLevel#isSerial() serial} consistency levels should never be - * passed to this method; attempting to do so would trigger an {@link - * com.datastax.driver.core.exceptions.InvalidQueryException InvalidQueryException}. - * - * @param consistency the consistency level to use for the retry; if {@code null}, the same - * level as the previous attempt will be used. - * @return a {@link RetryDecision.Type#RETRY} retry decision using the next host in the query - * plan, and using the provided consistency level. - */ - public static RetryDecision tryNextHost(ConsistencyLevel consistency) { - return new RetryDecision(Type.RETRY, consistency, false); - } - - @Override - public String toString() { - switch (type) { - case RETRY: - String retryClDesc = (retryCL == null) ? "same CL" : retryCL.toString(); - String hostDesc = retryCurrent ? "same" : "next"; - return "Retry at " + retryClDesc + " on " + hostDesc + " host."; - case RETHROW: - return "Rethrow"; - case IGNORE: - return "Ignore"; - } - throw new AssertionError(); - } - } - - /** - * Defines whether to retry and at which consistency level on a read timeout. - * - *

Note that this method may be called even if {@code requiredResponses >= receivedResponses} - * if {@code dataPresent} is {@code false} (see {@link - * com.datastax.driver.core.exceptions.ReadTimeoutException#wasDataRetrieved}). - * - * @param statement the original query that timed out. - * @param cl the requested consistency level of the read that timed out. Note that this can never - * be a {@link ConsistencyLevel#isSerial() serial} consistency level. - * @param requiredResponses the number of responses that were required to achieve the requested - * consistency level. - * @param receivedResponses the number of responses that had been received by the time the timeout - * exception was raised. - * @param dataRetrieved whether actual data (by opposition to data checksum) was present in the - * received responses. - * @param nbRetry the number of retry already performed for this operation. - * @return the retry decision. If {@code RetryDecision.RETHROW} is returned, a {@link - * com.datastax.driver.core.exceptions.ReadTimeoutException} will be thrown for the operation. - */ - RetryDecision onReadTimeout( - Statement statement, - ConsistencyLevel cl, - int requiredResponses, - int receivedResponses, - boolean dataRetrieved, - int nbRetry); - - /** - * Defines whether to retry and at which consistency level on a write timeout. - * - *

Note that if a statement is {@link Statement#isIdempotent() not idempotent}, the driver will - * never retry it on a write timeout (this method won't even be called). - * - * @param statement the original query that timed out. - * @param cl the requested consistency level of the write that timed out. If the timeout occurred - * at the "paxos" phase of a Lightweight - * transaction, then {@code cl} will actually be the requested {@link - * ConsistencyLevel#isSerial() serial} consistency level. Beware that serial consistency - * levels should never be passed to a {@link RetryDecision RetryDecision} as this would - * invariably trigger an {@link com.datastax.driver.core.exceptions.InvalidQueryException - * InvalidQueryException}. Also, when {@code cl} is {@link ConsistencyLevel#isSerial() - * serial}, then {@code writeType} is always {@link WriteType#CAS CAS}. - * @param writeType the type of the write that timed out. - * @param requiredAcks the number of acknowledgments that were required to achieve the requested - * consistency level. - * @param receivedAcks the number of acknowledgments that had been received by the time the - * timeout exception was raised. - * @param nbRetry the number of retry already performed for this operation. - * @return the retry decision. If {@code RetryDecision.RETHROW} is returned, a {@link - * com.datastax.driver.core.exceptions.WriteTimeoutException} will be thrown for the - * operation. - */ - RetryDecision onWriteTimeout( - Statement statement, - ConsistencyLevel cl, - WriteType writeType, - int requiredAcks, - int receivedAcks, - int nbRetry); - - /** - * Defines whether to retry and at which consistency level on an unavailable exception. - * - * @param statement the original query for which the consistency level cannot be achieved. - * @param cl the requested consistency level for the operation. If the operation failed at the - * "paxos" phase of a Lightweight - * transaction, then {@code cl} will actually be the requested {@link - * ConsistencyLevel#isSerial() serial} consistency level. Beware that serial consistency - * levels should never be passed to a {@link RetryDecision RetryDecision} as this would - * invariably trigger an {@link com.datastax.driver.core.exceptions.InvalidQueryException - * InvalidQueryException}. - * @param requiredReplica the number of replica that should have been (known) alive for the - * operation to be attempted. - * @param aliveReplica the number of replica that were know to be alive by the coordinator of the - * operation. - * @param nbRetry the number of retry already performed for this operation. - * @return the retry decision. If {@code RetryDecision.RETHROW} is returned, an {@link - * com.datastax.driver.core.exceptions.UnavailableException} will be thrown for the operation. - */ - RetryDecision onUnavailable( - Statement statement, ConsistencyLevel cl, int requiredReplica, int aliveReplica, int nbRetry); - - /** - * Defines whether to retry and at which consistency level on an unexpected error. - * - *

This method might be invoked in the following situations: - * - *

    - *
  1. On a client timeout, while waiting for the server response (see {@link - * SocketOptions#getReadTimeoutMillis()}); - *
  2. On a connection error (socket closed, etc.); - *
  3. When the contacted host replies with an {@code OVERLOADED} error, {@code SERVER_ERROR}, - * {@code READ_FAILURE} or {@code WRITE_FAILURE}. - *
- * - *

Note that when such an error occurs, there is no guarantee that the mutation has been - * applied server-side or not. Therefore, if a statement is {@link Statement#isIdempotent() not - * idempotent}, the driver will never retry it (this method won't even be called). - * - * @param statement the original query that failed. - * @param cl the requested consistency level for the operation. Note that this is not necessarily - * the achieved consistency level (if any), and it is never a {@link - * ConsistencyLevel#isSerial() serial} one. - * @param e the exception that caused this request to fail. - * @param nbRetry the number of retries already performed for this operation. - * @return the retry decision. If {@code RetryDecision.RETHROW} is returned, the {@link - * DriverException} passed to this method will be thrown for the operation. - */ - RetryDecision onRequestError( - Statement statement, ConsistencyLevel cl, DriverException e, int nbRetry); - - /** - * Gets invoked at cluster startup. - * - * @param cluster the cluster that this policy is associated with. - */ - void init(Cluster cluster); - - /** - * Gets invoked at cluster shutdown. - * - *

This gives the policy the opportunity to perform some cleanup, for instance stop threads - * that it might have started. - */ - void close(); -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/RollingCount.java b/driver-core/src/main/java/com/datastax/driver/core/policies/RollingCount.java deleted file mode 100644 index 8f061fa5b92..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/RollingCount.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicLongArray; -import java.util.concurrent.atomic.AtomicReference; - -/** A "rolling" count over a 1-minute sliding window. */ -class RollingCount { - // Divide the minute into 5-second intervals - private static final long INTERVAL_SIZE = TimeUnit.SECONDS.toNanos(5); - - // A circular buffer containing the counts over the 12 previous intervals. Their sum is the count - // we're looking for. - // If we're at t = 61s this would span [0,60[ - private final AtomicLongArray previousIntervals = new AtomicLongArray(12); - // The interval we're currently recording events for. It hasn't completed yet, so it's not - // included in the count. - // If we're at t = 61s this would span [60,65[ - // Note that we don't expect very high concurrency on RollingCount (it's used to count errors), so - // AtomicLong is - // good enough here. - private final AtomicLong currentInterval = new AtomicLong(); - // Other mutable state, grouped in an object for atomic updates - private final AtomicReference state; - private final Clock clock; - - RollingCount(Clock clock) { - this.state = new AtomicReference(new State(clock.nanoTime())); - this.clock = clock; - } - - void increment() { - add(1); - } - - void add(long amount) { - tickIfNecessary(); - currentInterval.addAndGet(amount); - } - - long get() { - tickIfNecessary(); - return state.get().totalCount; - } - - private void tickIfNecessary() { - State oldState = state.get(); - long newTick = clock.nanoTime(); - long age = newTick - oldState.lastTick; - if (age >= INTERVAL_SIZE) { - long currentCount = currentInterval.get(); - - long newIntervalStartTick = newTick - age % INTERVAL_SIZE; - long elapsedIntervals = Math.min(age / INTERVAL_SIZE, 12); - int newOffset = (int) ((oldState.offset + elapsedIntervals) % 12); - - long newTotal; - if (elapsedIntervals == 12) { - // We wrapped around the circular buffer, all our values are stale - // Don't mutate previousIntervals yet because this part of the code is still multi-threaded. - newTotal = 0; - } else { - // Add the current interval that just completed - newTotal = oldState.totalCount + currentCount; - // Subtract all elapsed intervals: they're either idle ones, or the one at the old offset - // that we're - // about to replace - for (int i = 1; i <= elapsedIntervals; i++) { - newTotal -= previousIntervals.get((newOffset + 12 - i) % 12); - } - } - - State newState = new State(newIntervalStartTick, newOffset, newTotal); - if (state.compareAndSet(oldState, newState)) { - // Only one thread gets here, so we can now: - // - reset the current count (don't use reset because other threads might already have - // started updating - // it) - currentInterval.addAndGet(-currentCount); - // - store the interval that just completed (or clear it if we wrapped) - previousIntervals.set(oldState.offset, elapsedIntervals < 12 ? currentCount : 0); - // - clear any idle interval - for (int i = 1; i < elapsedIntervals; i++) { - previousIntervals.set((newOffset + 12 - i) % 12, 0); - } - } - } - } - - static class State { - final long lastTick; // last time the state was modified - final int offset; // the offset that the current interval will replace once it's complete - final long totalCount; // cache of sum(previousIntervals) - - State(long lastTick) { - this(lastTick, 0, 0); - } - - State(long lastTick, int offset, long totalCount) { - this.lastTick = lastTick; - this.offset = offset; - this.totalCount = totalCount; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/RoundRobinPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/RoundRobinPolicy.java deleted file mode 100644 index 49b24f63e3f..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/RoundRobinPolicy.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Configuration; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.HostDistance; -import com.datastax.driver.core.Statement; -import com.google.common.collect.AbstractIterator; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Random; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicInteger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A Round-robin load balancing policy. - * - *

This policy queries nodes in a round-robin fashion. For a given query, if an host fail, the - * next one (following the round-robin order) is tried, until all hosts have been tried. - * - *

This policy is not datacenter aware and will include every known Cassandra host in its round - * robin algorithm. If you use multiple datacenter this will be inefficient and you will want to use - * the {@link DCAwareRoundRobinPolicy} load balancing policy instead. - */ -public class RoundRobinPolicy implements LoadBalancingPolicy { - - private static final Logger logger = LoggerFactory.getLogger(RoundRobinPolicy.class); - - private final CopyOnWriteArrayList liveHosts = new CopyOnWriteArrayList(); - private final AtomicInteger index = new AtomicInteger(); - - private volatile Configuration configuration; - private volatile boolean hasLoggedLocalCLUse; - - /** - * Creates a load balancing policy that picks host to query in a round robin fashion (on all the - * hosts of the Cassandra cluster). - */ - public RoundRobinPolicy() {} - - @Override - public void init(Cluster cluster, Collection hosts) { - this.liveHosts.addAll(hosts); - this.configuration = cluster.getConfiguration(); - this.index.set(new Random().nextInt(Math.max(hosts.size(), 1))); - } - - /** - * Return the HostDistance for the provided host. - * - *

This policy consider all nodes as local. This is generally the right thing to do in a single - * datacenter deployment. If you use multiple datacenter, see {@link DCAwareRoundRobinPolicy} - * instead. - * - * @param host the host of which to return the distance of. - * @return the HostDistance to {@code host}. - */ - @Override - public HostDistance distance(Host host) { - return HostDistance.LOCAL; - } - - /** - * Returns the hosts to use for a new query. - * - *

The returned plan will try each known host of the cluster. Upon each call to this method, - * the {@code i}th host of the plans returned will cycle over all the hosts of the cluster in a - * round-robin fashion. - * - * @param loggedKeyspace the keyspace currently logged in on for this query. - * @param statement the query for which to build the plan. - * @return a new query plan, i.e. an iterator indicating which host to try first for querying, - * which one to use as failover, etc... - */ - @Override - public Iterator newQueryPlan(String loggedKeyspace, Statement statement) { - - if (!hasLoggedLocalCLUse) { - ConsistencyLevel cl = - statement.getConsistencyLevel() == null - ? configuration.getQueryOptions().getConsistencyLevel() - : statement.getConsistencyLevel(); - if (cl.isDCLocal()) { - hasLoggedLocalCLUse = true; - logger.warn( - "Detected request at Consistency Level {} but the non-DC aware RoundRobinPolicy is in use. " - + "It is strongly advised to use DCAwareRoundRobinPolicy if you have multiple DCs/use DC-aware consistency levels " - + "(note: this message will only be logged once)", - cl); - } - } - - // We clone liveHosts because we want a version of the list that - // cannot change concurrently of the query plan iterator (this - // would be racy). We use clone() as it don't involve a copy of the - // underlying array (and thus we rely on liveHosts being a CopyOnWriteArrayList). - @SuppressWarnings("unchecked") - final List hosts = (List) liveHosts.clone(); - final int startIdx = index.getAndIncrement(); - - // Overflow protection; not theoretically thread safe but should be good enough - if (startIdx > Integer.MAX_VALUE - 10000) index.set(0); - - return new AbstractIterator() { - - private int idx = startIdx; - private int remaining = hosts.size(); - - @Override - protected Host computeNext() { - if (remaining <= 0) return endOfData(); - - remaining--; - int c = idx++ % hosts.size(); - if (c < 0) c += hosts.size(); - return hosts.get(c); - } - }; - } - - @Override - public void onUp(Host host) { - liveHosts.addIfAbsent(host); - } - - @Override - public void onDown(Host host) { - liveHosts.remove(host); - } - - @Override - public void onAdd(Host host) { - onUp(host); - } - - @Override - public void onRemove(Host host) { - onDown(host); - } - - @Override - public void close() { - // nothing to do - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/SpeculativeExecutionPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/SpeculativeExecutionPolicy.java deleted file mode 100644 index f1d01d5b273..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/SpeculativeExecutionPolicy.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.Statement; - -/** - * The policy that decides if the driver will send speculative queries to the next hosts when the - * current host takes too long to respond. - * - *

Note that only idempotent statements will be speculatively retried, see {@link - * com.datastax.driver.core.Statement#isIdempotent()} for more information. - */ -public interface SpeculativeExecutionPolicy { - /** - * Gets invoked at cluster startup. - * - * @param cluster the cluster that this policy is associated with. - */ - void init(Cluster cluster); - - /** - * Returns the plan to use for a new query. - * - * @param loggedKeyspace the currently logged keyspace (the one set through either {@link - * Cluster#connect(String)} or by manually doing a {@code USE} query) for the session on which - * this plan need to be built. This can be {@code null} if the corresponding session has no - * keyspace logged in. - * @param statement the query for which to build a plan. - * @return the plan. - */ - SpeculativeExecutionPlan newPlan(String loggedKeyspace, Statement statement); - - /** - * Gets invoked at cluster shutdown. - * - *

This gives the policy the opportunity to perform some cleanup, for instance stop threads - * that it might have started. - */ - void close(); - - /** - * A plan that governs speculative executions for a given query. - * - *

Each time a host is queried, {@link #nextExecution(Host)} is invoked to determine if and - * when a speculative query to the next host will be sent. - */ - interface SpeculativeExecutionPlan { - /** - * Returns the time before the next speculative query. - * - * @param lastQueried the host that was just queried. - * @return the time (in milliseconds) before a speculative query is sent to the next host. If - * negative, no speculative query will be sent. If zero, it will immediately send the - * execution. Note that, prior to version 3.3.1, zero meant "no speculative query", so - * custom policies written at that time may now start to schedule more executions than - * expected; make sure you use a negative value, not zero, to stop executions. - */ - long nextExecution(Host lastQueried); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/TokenAwarePolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/TokenAwarePolicy.java deleted file mode 100644 index 77c6aeb8571..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/TokenAwarePolicy.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Cluster; -import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.Host; -import com.datastax.driver.core.HostDistance; -import com.datastax.driver.core.Metadata; -import com.datastax.driver.core.ProtocolVersion; -import com.datastax.driver.core.Statement; -import com.google.common.collect.AbstractIterator; -import com.google.common.collect.Lists; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -/** - * A wrapper load balancing policy that adds token awareness to a child policy. - * - *

This policy encapsulates another policy. The resulting policy works in the following way: - * - *

    - *
  • the {@code distance} method is inherited from the child policy. - *
  • the iterator returned by the {@code newQueryPlan} method will first return the {@link - * HostDistance#LOCAL LOCAL} replicas for the query if possible (i.e. if the query's - * {@linkplain Statement#getRoutingKey(ProtocolVersion, CodecRegistry) routing key} is not - * {@code null} and if the {@linkplain Metadata#getReplicas(String, ByteBuffer) set of - * replicas} for that partition key is not empty). If no local replica can be either found or - * successfully contacted, the rest of the query plan will fallback to the child policy's one. - *
- * - * The exact order in which local replicas are returned is dictated by the {@linkplain - * ReplicaOrdering strategy} provided at instantiation. - * - *

Do note that only replicas for which the child policy's {@linkplain - * LoadBalancingPolicy#distance(Host) distance} method returns {@link HostDistance#LOCAL LOCAL} will - * be considered having priority. For example, if you wrap {@link DCAwareRoundRobinPolicy} with this - * token aware policy, replicas from remote data centers may only be returned after all the hosts of - * the local data center. - */ -public class TokenAwarePolicy implements ChainableLoadBalancingPolicy { - - /** Strategies for replica ordering. */ - public enum ReplicaOrdering { - - /** - * Order replicas by token ring topology, i.e. always return the "primary" replica first, then - * the second, etc., according to the placement of replicas around the token ring. - * - *

This strategy is the only one guaranteed to order replicas in a deterministic and constant - * way. This increases the effectiveness of server-side row caching (especially at consistency - * level ONE), but is more heavily impacted by hotspots, since the primary replica is always - * tried first. - */ - TOPOLOGICAL, - - /** - * Return replicas in a different, random order for each query plan. This is the default - * strategy. - * - *

This strategy fans out writes and thus can alleviate hotspots caused by "fat" partitions, - * but its randomness makes server-side caching less efficient. - */ - RANDOM, - - /** - * Return the replicas in the exact same order in which they appear in the child policy's query - * plan. - * - *

This is the only strategy that fully respects the child policy's replica ordering. Use it - * when it is important to keep that order intact (e.g. when using the {@link - * LatencyAwarePolicy}). - */ - NEUTRAL - } - - private final LoadBalancingPolicy childPolicy; - private final ReplicaOrdering replicaOrdering; - private volatile Metadata clusterMetadata; - private volatile ProtocolVersion protocolVersion; - private volatile CodecRegistry codecRegistry; - - /** - * Creates a new {@code TokenAware} policy. - * - * @param childPolicy the load balancing policy to wrap with token awareness. - * @param replicaOrdering the strategy to use to order replicas. - */ - public TokenAwarePolicy(LoadBalancingPolicy childPolicy, ReplicaOrdering replicaOrdering) { - this.childPolicy = childPolicy; - this.replicaOrdering = replicaOrdering; - } - - /** - * Creates a new {@code TokenAware} policy. - * - * @param childPolicy the load balancing policy to wrap with token awareness. - * @param shuffleReplicas whether or not to shuffle the replicas. If {@code true}, then the {@link - * ReplicaOrdering#RANDOM RANDOM} strategy will be used, otherwise the {@link - * ReplicaOrdering#TOPOLOGICAL TOPOLOGICAL} one will be used. - * @deprecated Use {@link #TokenAwarePolicy(LoadBalancingPolicy, ReplicaOrdering)} instead. This - * constructor will be removed in the next major release. - */ - @SuppressWarnings("DeprecatedIsStillUsed") - @Deprecated - public TokenAwarePolicy(LoadBalancingPolicy childPolicy, boolean shuffleReplicas) { - this(childPolicy, shuffleReplicas ? ReplicaOrdering.RANDOM : ReplicaOrdering.TOPOLOGICAL); - } - - /** - * Creates a new {@code TokenAware} policy with {@link ReplicaOrdering#RANDOM RANDOM} replica - * ordering. - * - * @param childPolicy the load balancing policy to wrap with token awareness. - */ - public TokenAwarePolicy(LoadBalancingPolicy childPolicy) { - this(childPolicy, ReplicaOrdering.RANDOM); - } - - @Override - public LoadBalancingPolicy getChildPolicy() { - return childPolicy; - } - - @Override - public void init(Cluster cluster, Collection hosts) { - clusterMetadata = cluster.getMetadata(); - protocolVersion = cluster.getConfiguration().getProtocolOptions().getProtocolVersion(); - codecRegistry = cluster.getConfiguration().getCodecRegistry(); - childPolicy.init(cluster, hosts); - } - - /** - * {@inheritDoc} - * - *

This implementation always returns distances as reported by the wrapped policy. - */ - @Override - public HostDistance distance(Host host) { - return childPolicy.distance(host); - } - - /** - * {@inheritDoc} - * - *

The returned plan will first return local replicas for the query (i.e. replicas whose - * {@linkplain HostDistance distance} according to the child policy is {@code LOCAL}), if it can - * determine them (i.e. mainly if the statement's {@linkplain - * Statement#getRoutingKey(ProtocolVersion, CodecRegistry) routing key} is not {@code null}), and - * ordered according to the {@linkplain ReplicaOrdering ordering strategy} specified at - * instantiation; following what it will return the rest of the child policy's original query - * plan. - */ - @Override - public Iterator newQueryPlan(final String loggedKeyspace, final Statement statement) { - - ByteBuffer partitionKey = statement.getRoutingKey(protocolVersion, codecRegistry); - String keyspace = statement.getKeyspace(); - if (keyspace == null) keyspace = loggedKeyspace; - - if (partitionKey == null || keyspace == null) - return childPolicy.newQueryPlan(keyspace, statement); - - final Set replicas = clusterMetadata.getReplicas(Metadata.quote(keyspace), partitionKey); - if (replicas.isEmpty()) return childPolicy.newQueryPlan(loggedKeyspace, statement); - - if (replicaOrdering == ReplicaOrdering.NEUTRAL) { - - final Iterator childIterator = childPolicy.newQueryPlan(keyspace, statement); - - return new AbstractIterator() { - - private List nonReplicas; - private Iterator nonReplicasIterator; - - @Override - protected Host computeNext() { - - while (childIterator.hasNext()) { - - Host host = childIterator.next(); - - if (host.isUp() - && replicas.contains(host) - && childPolicy.distance(host) == HostDistance.LOCAL) { - // UP replicas should be prioritized, retaining order from childPolicy - return host; - } else { - // save for later - if (nonReplicas == null) nonReplicas = new ArrayList(); - nonReplicas.add(host); - } - } - - // This should only engage if all local replicas are DOWN - if (nonReplicas != null) { - - if (nonReplicasIterator == null) nonReplicasIterator = nonReplicas.iterator(); - - if (nonReplicasIterator.hasNext()) return nonReplicasIterator.next(); - } - - return endOfData(); - } - }; - - } else { - - final Iterator replicasIterator; - - if (replicaOrdering == ReplicaOrdering.RANDOM) { - List replicasList = Lists.newArrayList(replicas); - Collections.shuffle(replicasList); - replicasIterator = replicasList.iterator(); - } else { - replicasIterator = replicas.iterator(); - } - - return new AbstractIterator() { - - private Iterator childIterator; - - @Override - protected Host computeNext() { - while (replicasIterator.hasNext()) { - Host host = replicasIterator.next(); - if (host.isUp() && childPolicy.distance(host) == HostDistance.LOCAL) return host; - } - - if (childIterator == null) - childIterator = childPolicy.newQueryPlan(loggedKeyspace, statement); - - while (childIterator.hasNext()) { - Host host = childIterator.next(); - // Skip it if it was already a local replica - if (!replicas.contains(host) || childPolicy.distance(host) != HostDistance.LOCAL) - return host; - } - return endOfData(); - } - }; - } - } - - @Override - public void onUp(Host host) { - childPolicy.onUp(host); - } - - @Override - public void onDown(Host host) { - childPolicy.onDown(host); - } - - @Override - public void onAdd(Host host) { - childPolicy.onAdd(host); - } - - @Override - public void onRemove(Host host) { - childPolicy.onRemove(host); - } - - @Override - public void close() { - childPolicy.close(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/WhiteListPolicy.java b/driver-core/src/main/java/com/datastax/driver/core/policies/WhiteListPolicy.java deleted file mode 100644 index b57bdb4b382..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/WhiteListPolicy.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.policies; - -import com.datastax.driver.core.Host; -import com.google.common.base.Predicate; -import com.google.common.collect.ImmutableSet; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.util.Arrays; -import java.util.Collection; - -/** - * A load balancing policy wrapper that ensure that only hosts from a provided white list will ever - * be returned. - * - *

This policy wraps another load balancing policy and will delegate the choice of hosts to the - * wrapped policy with the exception that only hosts contained in the white list provided when - * constructing this policy will ever be returned. Any host not in the while list will be considered - * {@code IGNORED} and thus will not be connected to. - * - *

This policy can be useful to ensure that the driver only connects to a predefined set of - * hosts. Keep in mind however that this policy defeats somewhat the host auto-detection of the - * driver. As such, this policy is only useful in a few special cases or for testing, but is not - * optimal in general. If all you want to do is limiting connections to hosts of the local - * data-center then you should use DCAwareRoundRobinPolicy and *not* this policy in particular. - * - * @see HostFilterPolicy - */ -public class WhiteListPolicy extends HostFilterPolicy { - - /** - * Creates a new policy that wraps the provided child policy but only "allows" hosts from the - * provided white list. - * - * @param childPolicy the wrapped policy. - * @param whiteList the white listed hosts. Only hosts from this list may get connected to - * (whether they will get connected to or not depends on the child policy). - */ - public WhiteListPolicy(LoadBalancingPolicy childPolicy, Collection whiteList) { - super(childPolicy, buildPredicate(whiteList)); - } - - /** - * Private constructor solely for maintaining type from policy created by {@link - * #ofHosts(LoadBalancingPolicy, String...)}. - */ - private WhiteListPolicy(LoadBalancingPolicy childPolicy, Predicate predicate) { - super(childPolicy, predicate); - } - - private static Predicate buildPredicate(Collection whiteList) { - final ImmutableSet hosts = ImmutableSet.copyOf(whiteList); - return new Predicate() { - @Override - public boolean apply(Host host) { - // This policy shouldn't be used with endpoints that don't resolve to unique addresses. This - // should be pretty obvious from the API. We don't really have any way to check it here. - InetSocketAddress socketAddress = host.getEndPoint().resolve(); - return hosts.contains(socketAddress); - } - }; - } - - /** - * Creates a new policy with the given host names. - * - *

See {@link #ofHosts(LoadBalancingPolicy, Iterable)} for more details. - */ - public static WhiteListPolicy ofHosts(LoadBalancingPolicy childPolicy, String... hostnames) { - return ofHosts(childPolicy, Arrays.asList(hostnames)); - } - - /** - * Creates a new policy that wraps the provided child policy but only "allows" hosts having - * addresses that match those from the resolved input host names. - * - *

Note that all host names must be non-null and resolvable; if any of them cannot be - * resolved, this method will fail. - * - * @param childPolicy the wrapped policy. - * @param hostnames list of host names to resolve whitelisted addresses from. - * @throws IllegalArgumentException if any of the given {@code hostnames} could not be resolved. - * @throws NullPointerException If null was provided for a hostname. - * @throws SecurityException if a security manager is present and permission to resolve the host - * name is denied. - */ - public static WhiteListPolicy ofHosts( - LoadBalancingPolicy childPolicy, Iterable hostnames) { - ImmutableSet.Builder builder = ImmutableSet.builder(); - for (String hostname : hostnames) { - try { - // We explicitly check for nulls because InetAdress.getByName() will happily - // accept it and use localhost (while a null here almost likely mean a user error, - // not "connect to localhost") - if (hostname == null) throw new NullPointerException(); - builder.add(InetAddress.getAllByName(hostname)); - } catch (UnknownHostException e) { - throw new IllegalArgumentException("Failed to resolve: " + hostname, e); - } - } - final ImmutableSet addresses = builder.build(); - return new WhiteListPolicy( - childPolicy, - new Predicate() { - @Override - public boolean apply(Host host) { - InetSocketAddress socketAddress = host.getEndPoint().resolve(); - return addresses.contains(socketAddress.getAddress()); - } - }); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/policies/package-info.java b/driver-core/src/main/java/com/datastax/driver/core/policies/package-info.java deleted file mode 100644 index 54117d969ec..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/policies/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Policies that allow to control some of the behavior of the DataStax Java driver for Cassandra. - */ -package com.datastax.driver.core.policies; diff --git a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Assignment.java b/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Assignment.java deleted file mode 100644 index be35ab6e3e7..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Assignment.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.querybuilder; - -import static com.datastax.driver.core.querybuilder.Utils.appendName; -import static com.datastax.driver.core.querybuilder.Utils.appendValue; - -import com.datastax.driver.core.CodecRegistry; -import java.util.List; - -public abstract class Assignment extends Utils.Appendeable { - - final Object name; - - private Assignment(Object name) { - this.name = name; - } - - /** - * The name of the column this assignment applies to. - * - * @return the name of the column this assignment applies to. - */ - public String getColumnName() { - return name.toString(); - } - - abstract boolean isIdempotent(); - - static class SetAssignment extends Assignment { - - private final Object value; - - SetAssignment(Object name, Object value) { - super(name); - this.value = value; - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - appendName(name, codecRegistry, sb); - sb.append('='); - appendValue(value, codecRegistry, sb, variables); - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(value); - } - - @Override - boolean isIdempotent() { - return Utils.isIdempotent(value); - } - } - - static class CounterAssignment extends Assignment { - - private final Object value; - private final boolean isIncr; - - CounterAssignment(String name, Object value, boolean isIncr) { - super(name); - if (!isIncr && value instanceof Long && ((Long) value) < 0) { - this.value = -((Long) value); - this.isIncr = true; - } else { - this.value = value; - this.isIncr = isIncr; - } - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - appendName(name, codecRegistry, sb).append('='); - appendName(name, codecRegistry, sb).append(isIncr ? "+" : "-"); - appendValue(value, codecRegistry, sb, variables); - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(value); - } - - @Override - boolean isIdempotent() { - return false; - } - } - - static class ListPrependAssignment extends Assignment { - - private final Object value; - - ListPrependAssignment(String name, Object value) { - super(name); - this.value = value; - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - appendName(name, codecRegistry, sb).append('='); - appendValue(value, codecRegistry, sb, variables); - sb.append('+'); - appendName(name, codecRegistry, sb); - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(value); - } - - @Override - boolean isIdempotent() { - return false; - } - } - - static class ListSetIdxAssignment extends Assignment { - - private final int idx; - private final Object value; - - ListSetIdxAssignment(String name, int idx, Object value) { - super(name); - this.idx = idx; - this.value = value; - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - appendName(name, codecRegistry, sb).append('[').append(idx).append("]="); - appendValue(value, codecRegistry, sb, variables); - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(value); - } - - @Override - boolean isIdempotent() { - return true; - } - } - - static class CollectionAssignment extends Assignment { - - private final Object collection; - private final boolean isAdd; - private final boolean isIdempotent; - - CollectionAssignment(String name, Object collection, boolean isAdd, boolean isIdempotent) { - super(name); - this.collection = collection; - this.isAdd = isAdd; - this.isIdempotent = isIdempotent; - } - - CollectionAssignment(String name, Object collection, boolean isAdd) { - this(name, collection, isAdd, true); - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - appendName(name, codecRegistry, sb).append('='); - appendName(name, codecRegistry, sb).append(isAdd ? "+" : "-"); - appendValue(collection, codecRegistry, sb, variables); - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(collection); - } - - @Override - public boolean isIdempotent() { - return isIdempotent; - } - } - - static class MapPutAssignment extends Assignment { - - private final Object key; - private final Object value; - - MapPutAssignment(String name, Object key, Object value) { - super(name); - this.key = key; - this.value = value; - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - appendName(name, codecRegistry, sb).append('['); - appendValue(key, codecRegistry, sb, variables); - sb.append("]="); - appendValue(value, codecRegistry, sb, variables); - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(key) || Utils.containsBindMarker(value); - } - - @Override - boolean isIdempotent() { - return true; - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Batch.java b/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Batch.java deleted file mode 100644 index 546176f852f..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Batch.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.querybuilder; - -import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.ProtocolVersion; -import com.datastax.driver.core.RegularStatement; -import com.datastax.driver.core.SimpleStatement; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -/** A built BATCH statement. */ -public class Batch extends BuiltStatement { - - private final List statements; - private final boolean logged; - private final Options usings; - - // Only used when we add at last one statement that is not a BuiltStatement subclass - private int nonBuiltStatementValues; - - Batch(RegularStatement[] statements, boolean logged) { - super(null, null, null); - this.statements = - statements.length == 0 - ? new ArrayList() - : new ArrayList(statements.length); - this.logged = logged; - this.usings = new Options(this); - - for (int i = 0; i < statements.length; i++) add(statements[i]); - } - - @Override - StringBuilder buildQueryString(List variables, CodecRegistry codecRegistry) { - StringBuilder builder = new StringBuilder(); - - builder.append( - isCounterOp() ? "BEGIN COUNTER BATCH" : (logged ? "BEGIN BATCH" : "BEGIN UNLOGGED BATCH")); - - if (!usings.usings.isEmpty()) { - builder.append(" USING "); - Utils.joinAndAppend(builder, codecRegistry, " AND ", usings.usings, variables); - } - builder.append(' '); - - for (int i = 0; i < statements.size(); i++) { - RegularStatement stmt = statements.get(i); - if (stmt instanceof BuiltStatement) { - BuiltStatement bst = (BuiltStatement) stmt; - builder.append(maybeAddSemicolon(bst.buildQueryString(variables, codecRegistry))); - - } else { - String str = stmt.getQueryString(codecRegistry); - builder.append(str); - if (!str.trim().endsWith(";")) builder.append(';'); - - // Note that we force hasBindMarkers if there is any non-BuiltStatement, so we know - // that we can only get there with variables == null - assert variables == null; - } - } - builder.append("APPLY BATCH;"); - return builder; - } - - /** - * Adds a new statement to this batch. - * - * @param statement the new statement to add. - * @return this batch. - * @throws IllegalArgumentException if counter and non-counter operations are mixed. - */ - public Batch add(RegularStatement statement) { - boolean isCounterOp = - statement instanceof BuiltStatement && ((BuiltStatement) statement).isCounterOp(); - - if (this.isCounterOp == null) setCounterOp(isCounterOp); - else if (isCounterOp() != isCounterOp) - throw new IllegalArgumentException( - "Cannot mix counter operations and non-counter operations in a batch statement"); - - this.statements.add(statement); - - if (statement instanceof BuiltStatement) { - this.hasBindMarkers |= ((BuiltStatement) statement).hasBindMarkers; - } else { - // For non-BuiltStatement, we cannot know if it includes a bind makers and we assume it does. - // In practice, - // this means we will always serialize values as strings when there is non-BuiltStatement - this.hasBindMarkers = true; - this.nonBuiltStatementValues += ((SimpleStatement) statement).valuesCount(); - } - - setDirty(); - - return this; - } - - @Override - public ByteBuffer[] getValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - // If there is some non-BuiltStatement inside the batch with values, we shouldn't - // use super.getValues() since it will ignore the values of said non-BuiltStatement. - // If that's the case, we just collects all those values (and we know - // super.getValues() == null in that case since we've explicitely set this.hasBindMarker - // to true). Otherwise, we simply call super.getValues(). - if (nonBuiltStatementValues == 0) return super.getValues(protocolVersion, codecRegistry); - - ByteBuffer[] values = new ByteBuffer[nonBuiltStatementValues]; - int i = 0; - for (RegularStatement statement : statements) { - if (statement instanceof BuiltStatement) continue; - - ByteBuffer[] statementValues = statement.getValues(protocolVersion, codecRegistry); - System.arraycopy(statementValues, 0, values, i, statementValues.length); - i += statementValues.length; - } - return values; - } - - /** - * Adds a new options for this BATCH statement. - * - * @param using the option to add. - * @return the options of this BATCH statement. - */ - public Options using(Using using) { - return usings.and(using); - } - - /** - * Returns the first non-null routing key of the statements in this batch or null otherwise. - * - * @return the routing key for this batch statement. - */ - @Override - public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - for (RegularStatement statement : statements) { - ByteBuffer routingKey = statement.getRoutingKey(protocolVersion, codecRegistry); - if (routingKey != null) { - return routingKey; - } - } - return null; - } - - /** - * Returns the keyspace of the first statement in this batch. - * - * @return the keyspace of the first statement in this batch. - */ - @Override - public String getKeyspace() { - return statements.isEmpty() ? null : statements.get(0).getKeyspace(); - } - - @Override - public Boolean isIdempotent() { - if (idempotent != null) { - return idempotent; - } - return isBatchIdempotent(statements); - } - - /** The options of a BATCH statement. */ - public static class Options extends BuiltStatement.ForwardingStatement { - - private final List usings = new ArrayList(); - - Options(Batch statement) { - super(statement); - } - - /** - * Adds the provided option. - * - * @param using a BATCH option. - * @return this {@code Options} object. - */ - public Options and(Using using) { - usings.add(using); - checkForBindMarkers(using); - return this; - } - - /** - * Adds a new statement to the BATCH statement these options are part of. - * - * @param statement the statement to add. - * @return the BATCH statement these options are part of. - */ - public Batch add(RegularStatement statement) { - return this.statement.add(statement); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/BindMarker.java b/driver-core/src/main/java/com/datastax/driver/core/querybuilder/BindMarker.java deleted file mode 100644 index 78719cafda5..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/BindMarker.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.querybuilder; - -/** - * A CQL3 bind marker. - * - *

This can be either an anonymous bind marker or a named one (but note that named ones are only - * supported starting in Cassandra 2.0.1). - * - *

Please note that to create a new bind maker object you should use {@link - * QueryBuilder#bindMarker()} (anonymous marker) or {@link QueryBuilder#bindMarker(String)} (named - * marker). - */ -public class BindMarker { - static final BindMarker ANONYMOUS = new BindMarker(null); - - private final String name; - - BindMarker(String name) { - this.name = name; - } - - @Override - public String toString() { - if (name == null) return "?"; - - return Utils.appendName(name, new StringBuilder(name.length() + 1).append(':')).toString(); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/BuiltStatement.java b/driver-core/src/main/java/com/datastax/driver/core/querybuilder/BuiltStatement.java deleted file mode 100644 index 7a880a70fbf..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/BuiltStatement.java +++ /dev/null @@ -1,460 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.querybuilder; - -import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.ColumnMetadata; -import com.datastax.driver.core.ConsistencyLevel; -import com.datastax.driver.core.Metadata; -import com.datastax.driver.core.ProtocolVersion; -import com.datastax.driver.core.RegularStatement; -import com.datastax.driver.core.Statement; -import com.datastax.driver.core.TypeCodec; -import com.datastax.driver.core.exceptions.CodecNotFoundException; -import com.datastax.driver.core.policies.RetryPolicy; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Common ancestor to statements generated with the {@link QueryBuilder}. - * - *

The actual query string will be generated and cached the first time it is requested, which is - * either when the driver tries to execute the query, or when you call certain public methods (for - * example {@link RegularStatement#getQueryString(CodecRegistry)}, {@link #getObject(int, - * CodecRegistry)}). - * - *

Whenever possible (and unless you call {@link #setForceNoValues(boolean)}, the builder will - * try to handle values passed to its methods as standalone values bound to the query string with - * placeholders. For instance: - * - *

- *     select().all().from("foo").where(eq("k", "the key"));
- *     // Is equivalent to:
- *     new SimpleStatement("SELECT * FROM foo WHERE k=?", "the key");
- * 
- * - * There are a few exceptions to this rule: - * - *
    - *
  • for fixed-size number types, the builder can't guess what the actual CQL type is. - * Standalone values are sent to Cassandra in their serialized form, and number types aren't - * all serialized in the same way, so picking the wrong type could lead to a query error; - *
  • if the value is a "special" element like a function call, it can't be serialized. This also - * applies to collections mixing special elements and regular objects. - *
- * - * In these cases, the builder will inline the value in the query string: - * - *
- *     select().all().from("foo").where(eq("k", 1));
- *     // Is equivalent to:
- *     new SimpleStatement("SELECT * FROM foo WHERE k=1");
- * 
- * - * One final thing to consider is {@link CodecRegistry custom codecs}. If you've registered codecs - * to handle your own Java types against the cluster, then you can pass instances of those types to - * query builder methods. But should the builder have to inline those values, it needs your codecs - * to {@link TypeCodec#format(Object) convert them to string form}. That is why some of the public - * methods of this class take a {@code CodecRegistry} as a parameter: - * - *
- *     BuiltStatement s = select().all().from("foo").where(eq("k", myCustomObject));
- *     // if we do this codecs will definitely be needed:
- *     s.forceNoValues(true);
- *     s.getQueryString(myCodecRegistry);
- * 
- * - * For convenience, there are no-arg versions of those methods that use {@link - * CodecRegistry#DEFAULT_INSTANCE}. But you should only use them if you are sure that no custom - * values will need to be inlined while building the statement, or if you have registered your - * custom codecs with the default registry instance. Otherwise, you will get a {@link - * CodecNotFoundException}. - */ -public abstract class BuiltStatement extends RegularStatement { - - private final List partitionKey; - private final List routingKeyValues; - final String keyspace; - - private boolean dirty; - private String cache; - private List values; - Boolean isCounterOp; - boolean hasNonIdempotentOps; - - // Whether the user has inputted bind markers. If that's the case, we never generate values as - // it means the user meant for the statement to be prepared and we shouldn't add our own markers. - boolean hasBindMarkers; - private boolean forceNoValues; - - BuiltStatement( - String keyspace, List partitionKey, List routingKeyValues) { - this.partitionKey = partitionKey; - this.routingKeyValues = routingKeyValues; - this.keyspace = keyspace; - } - - /** - * @deprecated preserved for backward compatibility, use {@link Metadata#quoteIfNecessary(String)} - * instead. - */ - @Deprecated - protected static String escapeId(String ident) { - return Metadata.quoteIfNecessary(ident); - } - - @Override - public String getQueryString(CodecRegistry codecRegistry) { - maybeRebuildCache(codecRegistry); - return cache; - } - - /** - * Returns the {@code i}th value as the Java type matching its CQL type. - * - * @param i the index to retrieve. - * @param codecRegistry the codec registry that will be used if the statement must be rebuilt in - * order to determine if it has values, and Java objects must be inlined in the process (see - * {@link BuiltStatement} for more explanations on why this is so). - * @return the value of the {@code i}th value of this statement. - * @throws IllegalStateException if this statement does not have values. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - * @see #getObject(int) - */ - public Object getObject(int i, CodecRegistry codecRegistry) { - maybeRebuildCache(codecRegistry); - if (values == null || values.isEmpty()) - throw new IllegalStateException("This statement does not have values"); - if (i < 0 || i >= values.size()) throw new ArrayIndexOutOfBoundsException(i); - return values.get(i); - } - - /** - * Returns the {@code i}th value as the Java type matching its CQL type. - * - *

This method calls {@link #getObject(int, CodecRegistry)} with {@link - * CodecRegistry#DEFAULT_INSTANCE}. It's safe to use if you don't use any custom codecs, or if - * your custom codecs are in the default registry; otherwise, use the other method and provide the - * registry that contains your codecs. - * - * @param i the index to retrieve. - * @return the value of the {@code i}th value of this statement. - * @throws IllegalStateException if this statement does not have values. - * @throws IndexOutOfBoundsException if {@code i} is not a valid index for this object. - */ - public Object getObject(int i) { - return getObject(i, CodecRegistry.DEFAULT_INSTANCE); - } - - private void maybeRebuildCache(CodecRegistry codecRegistry) { - if (!dirty && cache != null) return; - - StringBuilder sb; - values = null; - - if (hasBindMarkers || forceNoValues) { - sb = buildQueryString(null, codecRegistry); - } else { - values = new ArrayList(); - sb = buildQueryString(values, codecRegistry); - - if (values.size() > 65535) - throw new IllegalArgumentException( - "Too many values for built statement, the maximum allowed is 65535"); - - if (values.isEmpty()) values = null; - } - - maybeAddSemicolon(sb); - - cache = sb.toString(); - dirty = false; - } - - static StringBuilder maybeAddSemicolon(StringBuilder sb) { - // Use the same test that String#trim() uses to determine - // if a character is a whitespace character. - int l = sb.length(); - while (l > 0 && sb.charAt(l - 1) <= ' ') l -= 1; - if (l != sb.length()) sb.setLength(l); - - if (l == 0 || sb.charAt(l - 1) != ';') sb.append(';'); - return sb; - } - - abstract StringBuilder buildQueryString(List variables, CodecRegistry codecRegistry); - - boolean isCounterOp() { - return isCounterOp == null ? false : isCounterOp; - } - - void setCounterOp(boolean isCounterOp) { - this.isCounterOp = isCounterOp; - } - - boolean hasNonIdempotentOps() { - return hasNonIdempotentOps; - } - - void setNonIdempotentOps() { - hasNonIdempotentOps = true; - } - - void setDirty() { - dirty = true; - } - - void checkForBindMarkers(Object value) { - dirty = true; - if (Utils.containsBindMarker(value)) hasBindMarkers = true; - } - - void checkForBindMarkers(Utils.Appendeable value) { - dirty = true; - if (value != null && value.containsBindMarker()) hasBindMarkers = true; - } - - // TODO: Correctly document the InvalidTypeException - void maybeAddRoutingKey(String name, Object value) { - if (routingKeyValues == null - || name == null - || value == null - || Utils.containsSpecialValue(value)) return; - - for (int i = 0; i < partitionKey.size(); i++) { - if (Utils.handleId(name).equals(partitionKey.get(i).getName())) { - routingKeyValues.set(i, value); - return; - } - } - } - - @Override - public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - if (routingKeyValues == null) return null; - ByteBuffer[] routingKeyParts = new ByteBuffer[partitionKey.size()]; - for (int i = 0; i < partitionKey.size(); i++) { - Object value = routingKeyValues.get(i); - if (value == null) return null; - TypeCodec codec = codecRegistry.codecFor(partitionKey.get(i).getType(), value); - routingKeyParts[i] = codec.serialize(value, protocolVersion); - } - return routingKeyParts.length == 1 ? routingKeyParts[0] : Utils.compose(routingKeyParts); - } - - @Override - public String getKeyspace() { - return keyspace; - } - - @Override - public ByteBuffer[] getValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - maybeRebuildCache(codecRegistry); - return values == null ? null : Utils.convert(values.toArray(), protocolVersion, codecRegistry); - } - - @Override - public boolean hasValues(CodecRegistry codecRegistry) { - maybeRebuildCache(codecRegistry); - return values != null; - } - - @Override - public Map getNamedValues( - ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - // Built statements never return named values - return null; - } - - @Override - public boolean usesNamedValues() { - return false; - } - - @Override - public Boolean isIdempotent() { - // If a value was forced with setIdempotent, it takes priority - if (idempotent != null) return idempotent; - - // Otherwise return the computed value - return !hasNonIdempotentOps(); - } - - @Override - public String toString() { - try { - if (forceNoValues) return getQueryString(); - // 1) try first with all values inlined (will not work if some values require custom codecs, - // or if the required codecs are registered in a different CodecRegistry instance than the - // default one) - return maybeAddSemicolon(buildQueryString(null, CodecRegistry.DEFAULT_INSTANCE)).toString(); - } catch (RuntimeException e1) { - // 2) try next with bind markers for all values to avoid usage of custom codecs - try { - return maybeAddSemicolon( - buildQueryString(new ArrayList(), CodecRegistry.DEFAULT_INSTANCE)) - .toString(); - } catch (RuntimeException e2) { - // Ugly but we have absolutely no context to get the registry from - return String.format( - "built query (could not generate with default codec registry: %s)", e2.getMessage()); - } - } - } - - /** - * Allows to force this builder to not generate values (through its {@code getValues()} method). - * - *

By default (and unless the protocol version 1 is in use, see below) and for performance - * reasons, the query builder will not serialize all values provided to strings. This means that - * {@link #getQueryString(CodecRegistry)} may return a query string with bind markers (where and - * when is at the discretion of the builder) and {@link #getValues} will return the binary values - * for those markers. This method allows to force the builder to not generate binary values but - * rather to inline them all in the query string. In practice, this means that if you call {@code - * setForceNoValues(true)}, you are guaranteed that {@code getValues()} will return {@code null} - * and that the string returned by {@code getQueryString()} will contain no other bind markers - * than the ones specified by the user. - * - *

If the native protocol version 1 is in use, the driver will default to not generating values - * since those are not supported by that version of the protocol. In practice, the driver will - * automatically call this method with {@code true} as argument prior to execution. Hence, calling - * this method when the protocol version 1 is in use is basically a no-op. - * - *

Note that this method is mainly useful for debugging purpose. In general, the default - * behavior should be the correct and most efficient one. - * - * @param forceNoValues whether or not this builder may generate values. - * @return this statement. - */ - public RegularStatement setForceNoValues(boolean forceNoValues) { - this.forceNoValues = forceNoValues; - this.dirty = true; - return this; - } - - /** An utility class to create a BuiltStatement that encapsulate another one. */ - abstract static class ForwardingStatement extends BuiltStatement { - - T statement; - - ForwardingStatement(T statement) { - super(null, null, null); - this.statement = statement; - } - - @Override - public String getQueryString(CodecRegistry codecRegistry) { - return statement.getQueryString(codecRegistry); - } - - @Override - StringBuilder buildQueryString(List values, CodecRegistry codecRegistry) { - return statement.buildQueryString(values, codecRegistry); - } - - @Override - public ByteBuffer getRoutingKey(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - return statement.getRoutingKey(protocolVersion, codecRegistry); - } - - @Override - public String getKeyspace() { - return statement.getKeyspace(); - } - - @Override - boolean isCounterOp() { - return statement.isCounterOp(); - } - - @Override - boolean hasNonIdempotentOps() { - return statement.hasNonIdempotentOps(); - } - - @Override - public RegularStatement setForceNoValues(boolean forceNoValues) { - statement.setForceNoValues(forceNoValues); - return this; - } - - @Override - public Statement setConsistencyLevel(ConsistencyLevel consistency) { - statement.setConsistencyLevel(consistency); - return this; - } - - @Override - public ConsistencyLevel getConsistencyLevel() { - return statement.getConsistencyLevel(); - } - - @Override - public Statement enableTracing() { - statement.enableTracing(); - return this; - } - - @Override - public Statement disableTracing() { - statement.disableTracing(); - return this; - } - - @Override - public boolean isTracing() { - return statement.isTracing(); - } - - @Override - public Statement setRetryPolicy(RetryPolicy policy) { - statement.setRetryPolicy(policy); - return this; - } - - @Override - public RetryPolicy getRetryPolicy() { - return statement.getRetryPolicy(); - } - - @Override - public ByteBuffer[] getValues(ProtocolVersion protocolVersion, CodecRegistry codecRegistry) { - return statement.getValues(protocolVersion, codecRegistry); - } - - @Override - public boolean hasValues() { - return statement.hasValues(); - } - - @Override - void checkForBindMarkers(Object value) { - statement.checkForBindMarkers(value); - } - - @Override - void checkForBindMarkers(Utils.Appendeable value) { - statement.checkForBindMarkers(value); - } - - @Override - public String toString() { - return statement.toString(); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Clause.java b/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Clause.java deleted file mode 100644 index d6a3c1d35f2..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Clause.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.querybuilder; - -import com.datastax.driver.core.CodecRegistry; -import com.google.common.collect.Lists; -import java.util.List; - -public abstract class Clause extends Utils.Appendeable { - - abstract String name(); - - abstract Object firstValue(); - - private abstract static class AbstractClause extends Clause { - final String name; - - private AbstractClause(String name) { - this.name = name; - } - - @Override - String name() { - return name; - } - } - - static class SimpleClause extends AbstractClause { - - private final String op; - private final Object value; - - SimpleClause(String name, String op, Object value) { - super(name); - this.op = op; - this.value = value; - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - Utils.appendName(name, sb).append(op); - Utils.appendValue(value, codecRegistry, sb, variables); - } - - @Override - Object firstValue() { - return value; - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(value); - } - } - - static class IsNotNullClause extends AbstractClause { - - IsNotNullClause(String name) { - super(name); - } - - @Override - void appendTo(StringBuilder sb, List values, CodecRegistry codecRegistry) { - Utils.appendName(name, sb).append(" IS NOT NULL"); - } - - @Override - Object firstValue() { - return null; - } - - @Override - boolean containsBindMarker() { - return false; - } - } - - static class InClause extends AbstractClause { - - private final List values; - - InClause(String name, Iterable values) { - super(name); - if (values == null) throw new IllegalArgumentException("Missing values for IN clause"); - - this.values = Lists.newArrayList(values); - - if (this.values.size() > 65535) - throw new IllegalArgumentException( - "Too many values for IN clause, the maximum allowed is 65535"); - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - - // We special case the case of just one bind marker because there is little - // reasons to do: - // ... IN (?) ... - // since in that case it's more elegant to use an equal. On the other side, - // it is a lot more useful to do: - // ... IN ? ... - // which binds the variable to the full list the IN is on. - if (values.size() == 1 && values.get(0) instanceof BindMarker) { - Utils.appendName(name, sb).append(" IN ").append(values.iterator().next()); - return; - } - - Utils.appendName(name, sb).append(" IN ("); - Utils.joinAndAppendValues(sb, codecRegistry, values, variables).append(')'); - } - - @Override - Object firstValue() { - return values.isEmpty() ? null : values.get(0); - } - - @Override - boolean containsBindMarker() { - for (Object value : values) if (Utils.containsBindMarker(value)) return true; - return false; - } - } - - static class ContainsClause extends AbstractClause { - - private final Object value; - - ContainsClause(String name, Object value) { - super(name); - this.value = value; - - if (value == null) throw new IllegalArgumentException("Missing value for CONTAINS clause"); - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - Utils.appendName(name, sb).append(" CONTAINS "); - Utils.appendValue(value, codecRegistry, sb, variables); - } - - @Override - Object firstValue() { - return value; - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(value); - } - } - - static class ContainsKeyClause extends AbstractClause { - - private final Object value; - - ContainsKeyClause(String name, Object value) { - super(name); - this.value = value; - - if (value == null) - throw new IllegalArgumentException("Missing value for CONTAINS KEY clause"); - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - Utils.appendName(name, sb).append(" CONTAINS KEY "); - Utils.appendValue(value, codecRegistry, sb, variables); - } - - @Override - Object firstValue() { - return value; - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(value); - } - } - - static class CompoundClause extends Clause { - private String op; - private final List names; - private final List values; - - CompoundClause(Iterable names, String op, Iterable values) { - this.op = op; - this.names = Lists.newArrayList(names); - this.values = Lists.newArrayList(values); - if (this.names.size() != this.values.size()) - throw new IllegalArgumentException( - String.format( - "The number of names (%d) and values (%d) don't match", - this.names.size(), this.values.size())); - } - - @Override - String name() { - // This is only used for routing key purpose, and so far CompoundClause - // are not allowed for the partitionKey anyway - return null; - } - - @Override - Object firstValue() { - // This is only used for routing key purpose, and so far CompoundClause - // are not allowed for the partitionKey anyway - return null; - } - - @Override - boolean containsBindMarker() { - for (Object value : values) if (Utils.containsBindMarker(value)) return true; - return false; - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - sb.append("("); - for (int i = 0; i < names.size(); i++) { - if (i > 0) sb.append(","); - Utils.appendName(names.get(i), sb); - } - sb.append(")").append(op).append("("); - for (int i = 0; i < values.size(); i++) { - if (i > 0) sb.append(","); - Utils.appendValue(values.get(i), codecRegistry, sb, variables); - } - sb.append(")"); - } - } - - static class CompoundInClause extends Clause { - private final List names; - private final List valueLists; - - public CompoundInClause(Iterable names, Iterable valueLists) { - if (valueLists == null) throw new IllegalArgumentException("Missing values for IN clause"); - if (names == null) throw new IllegalArgumentException("Missing names for IN clause"); - - this.names = Lists.newArrayList(names); - this.valueLists = Lists.newArrayList(); - - for (Object value : valueLists) { - if (value instanceof Iterable) { - List tuple = Lists.newArrayList((Iterable) value); - if (tuple.size() != this.names.size()) { - throw new IllegalArgumentException( - String.format( - "The number of names (%d) and values (%d) don't match", - this.names.size(), tuple.size())); - } - this.valueLists.add(tuple); - } else if (!(value instanceof BindMarker)) { - throw new IllegalArgumentException( - String.format( - "Wrong element type for values list, expected List or BindMarker, got %s", - value.getClass().getName())); - } else { - this.valueLists.add(value); - } - } - if (this.valueLists.size() > 65535) - throw new IllegalArgumentException( - "Too many values for IN clause, the maximum allowed is 65535"); - } - - @Override - String name() { - // This is only used for routing key purpose, and so far CompoundClause - // are not allowed for the partitionKey anyway - return null; - } - - @Override - Object firstValue() { - // This is only used for routing key purpose, and so far CompoundClause - // are not allowed for the partitionKey anyway - return null; - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(valueLists); - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - sb.append("("); - for (int i = 0; i < names.size(); i++) { - if (i > 0) sb.append(","); - Utils.appendName(names.get(i), sb); - } - sb.append(")").append(" IN ").append("("); - for (int i = 0; i < valueLists.size(); i++) { - if (i > 0) sb.append(","); - - Object elt = valueLists.get(i); - - if (elt instanceof BindMarker) { - sb.append(elt); - } else { - List tuple = (List) elt; - if (tuple.size() == 1 && tuple.get(0) instanceof BindMarker) { - // Special case when there is only one bind marker: "IN ?" instead of "IN (?)" - sb.append(tuple.get(0)); - } else { - sb.append("("); - Utils.joinAndAppendValues(sb, codecRegistry, (List) tuple, variables).append(')'); - } - } - } - sb.append(")"); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Delete.java b/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Delete.java deleted file mode 100644 index 0ea36f9d435..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Delete.java +++ /dev/null @@ -1,560 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.querybuilder; - -import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.ColumnMetadata; -import com.datastax.driver.core.Metadata; -import com.datastax.driver.core.TableMetadata; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** A built DELETE statement. */ -public class Delete extends BuiltStatement { - - private final String table; - private final List columns; - private final Where where; - private final Options usings; - private final Conditions conditions; - private boolean ifExists; - - Delete(String keyspace, String table, List columns) { - this(keyspace, table, null, null, columns); - } - - Delete(TableMetadata table, List columns) { - this( - Metadata.quoteIfNecessary(table.getKeyspace().getName()), - Metadata.quoteIfNecessary(table.getName()), - Arrays.asList(new Object[table.getPartitionKey().size()]), - table.getPartitionKey(), - columns); - } - - Delete( - String keyspace, - String table, - List routingKeyValues, - List partitionKey, - List columns) { - super(keyspace, partitionKey, routingKeyValues); - this.table = table; - this.columns = columns; - this.where = new Where(this); - this.usings = new Options(this); - this.conditions = new Conditions(this); - - // This is for JAVA-1089, if the query deletes an element in a list, the statement should be - // non-idempotent. - if (!areIdempotent(columns)) { - setNonIdempotentOps(); - } - } - - @Override - StringBuilder buildQueryString(List variables, CodecRegistry codecRegistry) { - StringBuilder builder = new StringBuilder(); - - builder.append("DELETE"); - if (!columns.isEmpty()) - Utils.joinAndAppend(builder.append(" "), codecRegistry, ",", columns, variables); - - builder.append(" FROM "); - if (keyspace != null) Utils.appendName(keyspace, builder).append('.'); - Utils.appendName(table, builder); - if (!usings.usings.isEmpty()) { - builder.append(" USING "); - Utils.joinAndAppend(builder, codecRegistry, " AND ", usings.usings, variables); - } - - if (!where.clauses.isEmpty()) { - builder.append(" WHERE "); - Utils.joinAndAppend(builder, codecRegistry, " AND ", where.clauses, variables); - } - - if (ifExists) { - builder.append(" IF EXISTS "); - } - - if (!conditions.conditions.isEmpty()) { - builder.append(" IF "); - Utils.joinAndAppend(builder, codecRegistry, " AND ", conditions.conditions, variables); - } - - return builder; - } - - /** - * Adds a WHERE clause to this statement. - * - *

This is a shorter/more readable version for {@code where().and(clause)}. - * - * @param clause the clause to add. - * @return the where clause of this query to which more clause can be added. - */ - public Where where(Clause clause) { - return where.and(clause); - } - - /** - * Returns a Where statement for this query without adding clause. - * - * @return the where clause of this query to which more clause can be added. - */ - public Where where() { - return where; - } - - /** - * Adds a conditions clause (IF) to this statement. - * - *

This is a shorter/more readable version for {@code onlyIf().and(condition)}. - * - *

This will configure the statement as non-idempotent, see {@link - * com.datastax.driver.core.Statement#isIdempotent()} for more information. - * - * @param condition the condition to add. - * @return the conditions of this query to which more conditions can be added. - */ - public Conditions onlyIf(Clause condition) { - return conditions.and(condition); - } - - /** - * Adds a conditions clause (IF) to this statement. - * - *

This will configure the statement as non-idempotent, see {@link - * com.datastax.driver.core.Statement#isIdempotent()} for more information. - * - * @return the conditions of this query to which more conditions can be added. - */ - public Conditions onlyIf() { - return conditions; - } - - /** - * Adds a new options for this DELETE statement. - * - * @param using the option to add. - * @return the options of this DELETE statement. - */ - public Options using(Using using) { - return usings.and(using); - } - - /** - * Returns the options for this DELETE statement. - * - *

Chain this with {@link Options#and(Using)} to add options. - * - * @return the options of this DELETE statement. - */ - public Options using() { - return usings; - } - - /** - * Sets the 'IF EXISTS' option for this DELETE statement. - * - *

- * - *

A delete with that option will report whether the statement actually resulted in data being - * deleted. The existence check and deletion are done transactionally in the sense that if - * multiple clients attempt to delete a given row with this option, then at most one may succeed. - * - *

Please keep in mind that using this option has a non negligible performance impact and - * should be avoided when possible. This will configure the statement as non-idempotent, see - * {@link com.datastax.driver.core.Statement#isIdempotent()} for more information. - * - * @return this DELETE statement. - */ - public Delete ifExists() { - this.ifExists = true; - setNonIdempotentOps(); - return this; - } - - /** The WHERE clause of a DELETE statement. */ - public static class Where extends BuiltStatement.ForwardingStatement { - - private final List clauses = new ArrayList(); - - Where(Delete statement) { - super(statement); - } - - /** - * Adds the provided clause to this WHERE clause. - * - * @param clause the clause to add. - * @return this WHERE clause. - */ - public Where and(Clause clause) { - clauses.add(clause); - statement.maybeAddRoutingKey(clause.name(), clause.firstValue()); - if (!hasNonIdempotentOps() && !Utils.isIdempotent(clause)) { - statement.setNonIdempotentOps(); - } - checkForBindMarkers(clause); - return this; - } - - /** - * Adds an option to the DELETE statement this WHERE clause is part of. - * - * @param using the using clause to add. - * @return the options of the DELETE statement this WHERE clause is part of. - */ - public Options using(Using using) { - return statement.using(using); - } - - /** - * Sets the 'IF EXISTS' option for the DELETE statement this WHERE clause is part of. - * - *

- * - *

A delete with that option will report whether the statement actually resulted in data - * being deleted. The existence check and deletion are done transactionally in the sense that if - * multiple clients attempt to delete a given row with this option, then at most one may - * succeed. - * - *

Please keep in mind that using this option has a non negligible performance impact and - * should be avoided when possible. - * - * @return the DELETE statement this WHERE clause is part of. - */ - public Delete ifExists() { - return statement.ifExists(); - } - - /** - * Adds a condition to the DELETE statement this WHERE clause is part of. - * - * @param condition the condition to add. - * @return the conditions for the DELETE statement this WHERE clause is part of. - */ - public Conditions onlyIf(Clause condition) { - return statement.onlyIf(condition); - } - } - - /** The options of a DELETE statement. */ - public static class Options extends BuiltStatement.ForwardingStatement { - - private final List usings = new ArrayList(); - - Options(Delete statement) { - super(statement); - } - - /** - * Adds the provided option. - * - * @param using a DELETE option. - * @return this {@code Options} object. - */ - public Options and(Using using) { - usings.add(using); - checkForBindMarkers(using); - return this; - } - - /** - * Adds a where clause to the DELETE statement these options are part of. - * - * @param clause clause to add. - * @return the WHERE clause of the DELETE statement these options are part of. - */ - public Where where(Clause clause) { - return statement.where(clause); - } - } - - /** An in-construction DELETE statement. */ - public static class Builder { - - List columns = new ArrayList(); - - Builder() {} - - Builder(String... columnNames) { - for (String columnName : columnNames) { - this.columns.add(new Selector(columnName)); - } - } - - /** - * Adds the table to delete from. - * - * @param table the name of the table to delete from. - * @return a newly built DELETE statement that deletes from {@code table}. - */ - public Delete from(String table) { - return from(null, table); - } - - /** - * Adds the table to delete from. - * - * @param keyspace the name of the keyspace to delete from. - * @param table the name of the table to delete from. - * @return a newly built DELETE statement that deletes from {@code keyspace.table}. - */ - public Delete from(String keyspace, String table) { - return new Delete(keyspace, table, columns); - } - - /** - * Adds the table to delete from. - * - * @param table the table to delete from. - * @return a newly built DELETE statement that deletes from {@code table}. - */ - public Delete from(TableMetadata table) { - return new Delete(table, columns); - } - } - - /** An column selection clause for an in-construction DELETE statement. */ - public static class Selection extends Builder { - - /** - * Deletes all columns (i.e. "DELETE FROM ...") - * - * @return an in-build DELETE statement. - * @throws IllegalStateException if some columns had already been selected for this builder. - */ - public Builder all() { - if (!columns.isEmpty()) - throw new IllegalStateException( - String.format("Some columns (%s) have already been selected.", columns)); - - return this; - } - - /** - * Deletes the provided column. - * - * @param columnName the column to select for deletion. - * @return this in-build DELETE Selection - */ - public Selection column(String columnName) { - columns.add(new Selector(columnName)); - return this; - } - - /** - * Deletes the provided list element. - * - * @param columnName the name of the list column. - * @param idx the index of the element to delete. - * @return this in-build DELETE Selection - */ - public Selection listElt(String columnName, int idx) { - columns.add(new ListElementSelector(columnName, idx)); - return this; - } - - /** - * Deletes the provided list element, specified as a bind marker. - * - * @param columnName the name of the list column. - * @param idx the index of the element to delete. - * @return this in-build DELETE Selection - */ - public Selection listElt(String columnName, BindMarker idx) { - columns.add(new ListElementSelector(columnName, idx)); - return this; - } - - /** - * Deletes the provided set element. - * - * @param columnName the name of the set column. - * @param element the element to delete. - * @return this in-build DELETE Selection - */ - public Selection setElt(String columnName, Object element) { - columns.add(new SetElementSelector(columnName, element)); - return this; - } - - /** - * Deletes the provided set element, specified as a bind marker. - * - * @param columnName the name of the set column. - * @param element the element to delete. - * @return this in-build DELETE Selection - */ - public Selection setElt(String columnName, BindMarker element) { - columns.add(new SetElementSelector(columnName, element)); - return this; - } - - /** - * Deletes a map element given a key. - * - * @param columnName the name of the map column. - * @param key the key for the element to delete. - * @return this in-build DELETE Selection - */ - public Selection mapElt(String columnName, Object key) { - columns.add(new MapElementSelector(columnName, key)); - return this; - } - } - - /** - * A selector in a DELETE selection clause. A selector can be either a column name, a list - * element, a set element or a map entry. - */ - private static class Selector extends Utils.Appendeable { - - private final String columnName; - - Selector(String columnName) { - this.columnName = columnName; - } - - @Override - void appendTo(StringBuilder sb, List values, CodecRegistry codecRegistry) { - Utils.appendName(columnName, sb); - } - - @Override - boolean containsBindMarker() { - return false; - } - - @Override - public String toString() { - return columnName; - } - } - - /** - * A selector representing a list index, a set element or a map key in a DELETE selection clause. - */ - private static class CollectionElementSelector extends Selector { - - protected final Object key; - - CollectionElementSelector(String columnName, Object key) { - super(columnName); - this.key = key; - } - - @Override - void appendTo(StringBuilder sb, List values, CodecRegistry codecRegistry) { - super.appendTo(sb, values, codecRegistry); - sb.append('['); - Utils.appendValue(key, codecRegistry, sb, values); - sb.append(']'); - } - - @Override - boolean containsBindMarker() { - return Utils.containsBindMarker(key); - } - } - - private static class ListElementSelector extends CollectionElementSelector { - - ListElementSelector(String columnName, Object key) { - super(columnName, key); - } - } - - private static class SetElementSelector extends CollectionElementSelector { - - SetElementSelector(String columnName, Object key) { - super(columnName, key); - } - } - - private static class MapElementSelector extends CollectionElementSelector { - - MapElementSelector(String columnName, Object key) { - super(columnName, key); - } - } - - private boolean areIdempotent(List selectors) { - for (Selector sel : selectors) { - if (sel instanceof ListElementSelector) { - return false; - } - } - return true; - } - - /** - * Conditions for a DELETE statement. - * - *

When provided some conditions, a deletion will not apply unless the provided conditions - * applies. - * - *

Please keep in mind that provided conditions have a non negligible performance impact and - * should be avoided when possible. - */ - public static class Conditions extends BuiltStatement.ForwardingStatement { - - private final List conditions = new ArrayList(); - - Conditions(Delete statement) { - super(statement); - } - - /** - * Adds the provided condition for the deletion. - * - *

Note that while the query builder accept any type of {@code Clause} as conditions, - * Cassandra currently only allows equality ones. - * - * @param condition the condition to add. - * @return this {@code Conditions} clause. - */ - public Conditions and(Clause condition) { - statement.setNonIdempotentOps(); - conditions.add(condition); - checkForBindMarkers(condition); - return this; - } - - /** - * Adds a where clause to the DELETE statement these conditions are part of. - * - * @param clause clause to add. - * @return the WHERE clause of the DELETE statement these conditions are part of. - */ - public Where where(Clause clause) { - return statement.where(clause); - } - - /** - * Adds an option to the DELETE statement these conditions are part of. - * - * @param using the using clause to add. - * @return the options of the DELETE statement these conditions are part of. - */ - public Options using(Using using) { - return statement.using(using); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Insert.java b/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Insert.java deleted file mode 100644 index 7c0a8978918..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Insert.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.querybuilder; - -import static com.google.common.base.Preconditions.checkState; - -import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.ColumnMetadata; -import com.datastax.driver.core.Metadata; -import com.datastax.driver.core.TableMetadata; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** A built {@code INSERT} statement. */ -public class Insert extends BuiltStatement { - - private enum JsonDefault { - NULL, - UNSET - } - - private final String table; - private final List names = new ArrayList(); - private final List values = new ArrayList(); - private final Options usings; - private boolean ifNotExists; - private Object json; - private JsonDefault jsonDefault; - - Insert(String keyspace, String table) { - this(keyspace, table, null, null); - } - - Insert(TableMetadata table) { - this( - Metadata.quoteIfNecessary(table.getKeyspace().getName()), - Metadata.quoteIfNecessary(table.getName()), - Arrays.asList(new Object[table.getPartitionKey().size()]), - table.getPartitionKey()); - } - - Insert( - String keyspace, - String table, - List routingKeyValues, - List partitionKey) { - super(keyspace, partitionKey, routingKeyValues); - this.table = table; - this.usings = new Options(this); - } - - @Override - StringBuilder buildQueryString(List variables, CodecRegistry codecRegistry) { - StringBuilder builder = new StringBuilder(); - - builder.append("INSERT INTO "); - if (keyspace != null) Utils.appendName(keyspace, builder).append('.'); - Utils.appendName(table, builder); - - builder.append(" "); - if (json != null) { - builder.append("JSON "); - Utils.appendValue(json, codecRegistry, builder, variables); - if (jsonDefault == JsonDefault.UNSET) builder.append(" DEFAULT UNSET"); - else if (jsonDefault == JsonDefault.NULL) builder.append(" DEFAULT NULL"); - } else { - builder.append("("); - Utils.joinAndAppendNames(builder, codecRegistry, names); - builder.append(") VALUES ("); - Utils.joinAndAppendValues(builder, codecRegistry, values, variables); - builder.append(')'); - } - - if (ifNotExists) builder.append(" IF NOT EXISTS"); - - if (!usings.usings.isEmpty()) { - builder.append(" USING "); - Utils.joinAndAppend(builder, codecRegistry, " AND ", usings.usings, variables); - } - return builder; - } - - /** - * Adds a column/value pair to the values inserted by this {@code INSERT} statement. - * - * @param name the name of the column to insert/update. - * @param value the value to insert/update for {@code name}. - * @return this {@code INSERT} statement. - * @throws IllegalStateException if this method is called and the {@link #json(Object)} method has - * been called before, because it's not possible to mix {@code INSERT JSON} syntax with - * regular {@code INSERT} syntax. - */ - public Insert value(String name, Object value) { - checkState( - json == null && jsonDefault == null, - "Cannot mix INSERT JSON syntax with regular INSERT syntax"); - names.add(name); - values.add(value); - checkForBindMarkers(value); - if (!hasNonIdempotentOps() && !Utils.isIdempotent(value)) this.setNonIdempotentOps(); - maybeAddRoutingKey(name, value); - return this; - } - - /** - * Adds multiple column/value pairs to the values inserted by this INSERT statement. - * - * @param names a list of column names to insert/update. - * @param values a list of values to insert/update. The {@code i}th value in {@code values} will - * be inserted for the {@code i}th column in {@code names}. - * @return this INSERT statement. - * @throws IllegalArgumentException if {@code names.length != values.length}. - * @throws IllegalStateException if this method is called and the {@link #json(Object)} method has - * been called before, because it's not possible to mix {@code INSERT JSON} syntax with - * regular {@code INSERT} syntax. - */ - public Insert values(String[] names, Object[] values) { - return values(Arrays.asList(names), Arrays.asList(values)); - } - - /** - * Adds multiple column/value pairs to the values inserted by this INSERT statement. - * - * @param names a list of column names to insert/update. - * @param values a list of values to insert/update. The {@code i}th value in {@code values} will - * be inserted for the {@code i}th column in {@code names}. - * @return this INSERT statement. - * @throws IllegalArgumentException if {@code names.size() != values.size()}. - * @throws IllegalStateException if this method is called and the {@link #json(Object)} method has - * been called before, because it's not possible to mix {@code INSERT JSON} syntax with - * regular {@code INSERT} syntax. - */ - public Insert values(List names, List values) { - if (names.size() != values.size()) - throw new IllegalArgumentException( - String.format("Got %d names but %d values", names.size(), values.size())); - checkState( - json == null && jsonDefault == null, - "Cannot mix INSERT JSON syntax with regular INSERT syntax"); - this.names.addAll(names); - this.values.addAll(values); - for (int i = 0; i < names.size(); i++) { - Object value = values.get(i); - checkForBindMarkers(value); - maybeAddRoutingKey(names.get(i), value); - if (!hasNonIdempotentOps() && !Utils.isIdempotent(value)) this.setNonIdempotentOps(); - } - return this; - } - - /** - * Inserts the provided object, using the {@code INSERT INTO ... JSON} syntax introduced in - * Cassandra 2.2. - * - *

With INSERT statements, the new {@code JSON} keyword can be used to enable inserting a JSON - * structure as a single row. - * - *

The provided object can be of the following types: - * - *

    - *
  1. A raw string. In this case, it will be appended to the query string as is. It - * should NOT be surrounded by single quotes. Its format should generally match - * that returned by a {@code SELECT JSON} statement on the same table. Note that it is not - * possible to insert function calls nor bind markers in a JSON string. - *
  2. A {@link QueryBuilder#bindMarker() bind marker}. In this case, the statement is meant to - * be prepared and no JSON string will be appended to the query string, only a bind marker - * for the whole JSON parameter. - *
  3. Any object that can be serialized to JSON. Such objects can be used provided that a - * matching {@link com.datastax.driver.core.TypeCodec codec} is registered with the {@link - * CodecRegistry} in use. This allows the usage of JSON libraries, such as the Java API for JSON processing, the popular - * Jackson library, or Google's Gson library, for instance. - *
- * - *

Case-sensitive column names

- * - * When passing raw strings to this method, users are required to handle case-sensitive column - * names by surrounding them with double quotes. - * - *

For example, to insert into a table with two columns named “myKey” and “value”, you would do - * the following: - * - *

-   * insertInto("mytable").json("{\"\\\"myKey\\\"\": 0, \"value\": 0}");
-   * 
- * - * This will produce the following CQL: - * - *
-   * INSERT INTO mytable JSON '{"\"myKey\"": 0, "value": 0}';
-   * 
- * - *

Escaping quotes in column values

- * - * When passing raw strings to this method, double quotes should be escaped with a backslash, but - * single quotes should be escaped in the CQL manner, i.e. by another single quote. For example, - * the column value {@code foo"'bar} should be inserted in the JSON string as {@code - * "foo\"''bar"}. - * - *

- * - *

Null values and tombstones

- * - * Any columns which are omitted from the JSON string will be defaulted to a {@code NULL} value - * (which will result in a tombstone being created). - * - * @param json the JSON string, or a bind marker, or a JSON object handled by a specific {@link - * com.datastax.driver.core.TypeCodec codec}. - * @return this INSERT statement. - * @throws IllegalStateException if this method is called and any of the {@code value} or {@code - * values} methods have been called before, because it's not possible to mix {@code INSERT - * JSON} syntax with regular {@code INSERT} syntax. - * @see JSON Support for CQL - * @see JSON - * Support in Cassandra 2.2 - * @see Inserting - * JSON data - */ - public Insert json(Object json) { - checkState( - values.isEmpty() && names.isEmpty(), - "Cannot mix INSERT JSON syntax with regular INSERT syntax"); - this.json = json; - return this; - } - - /** - * Appends a {@code DEFAULT UNSET} clause to this {@code INSERT INTO ... JSON} statement. - * - *

Support for {@code DEFAULT UNSET} has been introduced in Cassandra 3.10. - * - * @return this {@code INSERT} statement. - * @throws IllegalStateException if this method is called and any of the {@code value} or {@code - * values} methods have been called before, because it's not possible to mix {@code INSERT - * JSON} syntax with regular {@code INSERT} syntax. - */ - public Insert defaultUnset() { - checkState( - values.isEmpty() && names.isEmpty(), - "Cannot mix INSERT JSON syntax with regular INSERT syntax"); - this.jsonDefault = JsonDefault.UNSET; - return this; - } - - /** - * Appends a {@code DEFAULT NULL} clause to this {@code INSERT INTO ... JSON} statement. - * - *

Support for {@code DEFAULT NULL} has been introduced in Cassandra 3.10. - * - * @return this {@code INSERT} statement. - * @throws IllegalStateException if this method is called and any of the {@code value} or {@code - * values} methods have been called before, because it's not possible to mix {@code INSERT - * JSON} syntax with regular {@code INSERT} syntax. - */ - public Insert defaultNull() { - checkState( - values.isEmpty() && names.isEmpty(), - "Cannot mix INSERT JSON syntax with regular INSERT syntax"); - this.jsonDefault = JsonDefault.NULL; - return this; - } - - /** - * Adds a new options for this {@code INSERT} statement. - * - * @param using the option to add. - * @return the options of this {@code INSERT} statement. - */ - public Options using(Using using) { - return usings.and(using); - } - - /** - * Returns the options for this {@code INSERT} statement. - * - *

Chain this with {@link Options#and(Using)} to add options. - * - * @return the options of this {@code INSERT} statement. - */ - public Options using() { - return usings; - } - - /** - * Sets the 'IF NOT EXISTS' option for this {@code INSERT} statement. - * - *

An insert with that option will not succeed unless the row does not exist at the time the - * insertion is executed. The existence check and insertions are done transactionally in the sense - * that if multiple clients attempt to create a given row with this option, then at most one may - * succeed. - * - *

Please keep in mind that using this option has a non negligible performance impact and - * should be avoided when possible. - * - *

This will configure the statement as non-idempotent, see {@link - * com.datastax.driver.core.Statement#isIdempotent()} for more information. - * - * @return this {@code INSERT} statement. - */ - public Insert ifNotExists() { - this.setNonIdempotentOps(); - this.ifNotExists = true; - return this; - } - - /** The options of an {@code INSERT} statement. */ - public static class Options extends BuiltStatement.ForwardingStatement { - - private final List usings = new ArrayList(); - - Options(Insert st) { - super(st); - } - - /** - * Adds the provided option. - * - * @param using an {@code INSERT} option. - * @return this {@code Options} object. - */ - public Options and(Using using) { - usings.add(using); - checkForBindMarkers(using); - return this; - } - - /** - * Adds a column/value pair to the values inserted by this {@code INSERT} statement. - * - * @param name the name of the column to insert/update. - * @param value the value to insert/update for {@code name}. - * @return the {@code INSERT} statement those options are part of. - */ - public Insert value(String name, Object value) { - return statement.value(name, value); - } - - /** - * Adds multiple column/value pairs to the values inserted by this {@code INSERT} statement. - * - * @param names a list of column names to insert/update. - * @param values a list of values to insert/update. The {@code i}th value in {@code values} will - * be inserted for the {@code i}th column in {@code names}. - * @return the {@code INSERT} statement those options are part of. - * @throws IllegalArgumentException if {@code names.length != values.length}. - */ - public Insert values(String[] names, Object[] values) { - return statement.values(names, values); - } - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Ordering.java b/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Ordering.java deleted file mode 100644 index 52c83184e02..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Ordering.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.querybuilder; - -import com.datastax.driver.core.CodecRegistry; -import java.util.List; - -public class Ordering extends Utils.Appendeable { - - private final String name; - private final boolean isDesc; - - Ordering(String name, boolean isDesc) { - this.name = name; - this.isDesc = isDesc; - } - - @Override - void appendTo(StringBuilder sb, List variables, CodecRegistry codecRegistry) { - Utils.appendName(name, sb); - sb.append(isDesc ? " DESC" : " ASC"); - } - - @Override - boolean containsBindMarker() { - return false; - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/QueryBuilder.java b/driver-core/src/main/java/com/datastax/driver/core/querybuilder/QueryBuilder.java deleted file mode 100644 index 5e09d0cef9c..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/QueryBuilder.java +++ /dev/null @@ -1,1325 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.querybuilder; - -import com.datastax.driver.core.DataType; -import com.datastax.driver.core.Metadata; -import com.datastax.driver.core.RegularStatement; -import com.datastax.driver.core.TableMetadata; -import com.datastax.driver.core.exceptions.InvalidQueryException; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Builds CQL3 query via a fluent API. - * - *

The queries built by this builder will provide a value for the {@link - * com.datastax.driver.core.Statement#getRoutingKey} method only when a {@link - * com.datastax.driver.core.TableMetadata} is provided to the builder. It is thus advised to do so - * if a {@link com.datastax.driver.core.policies.TokenAwarePolicy} is in use. - * - *

The provider builders perform very little validation of the built query. There is thus no - * guarantee that a built query is valid, and it is definitively possible to create invalid queries. - * - *

Note that it could be convenient to use an 'import static' to bring the static methods of this - * class into scope. - */ -public final class QueryBuilder { - - private QueryBuilder() {} - - /** - * Starts building a new {@code SELECT} query that selects the provided names. - * - *

Note that {@code select(c1, c2)} is just a shortcut for {@code - * select().column(c1).column(c2)}. - * - * @param columns the columns names that should be selected by the query. - * @return an in-construction {@code SELECT} query (you will need to provide at least a {@code - * FROM} clause to complete the query). - */ - public static Select.Builder select(String... columns) { - return select((Object[]) columns); - } - - /** - * Starts building a new {@code SELECT} query that selects the provided names. - * - *

Note that {@code select(c1, c2)} is just a shortcut for {@code - * select().column(c1).column(c2)}. - * - * @param columns the columns names that should be selected by the query. - * @return an in-construction {@code SELECT} query (you will need to provide at least a {@code - * FROM} clause to complete the query). - */ - public static Select.Builder select(Object... columns) { - return new Select.Builder(Arrays.asList(columns)); - } - - /** - * Starts building a new {@code SELECT} query. - * - * @return an in-construction {@code SELECT} query (you will need to provide a column selection - * and at least a {@code FROM} clause to complete the query). - */ - public static Select.Selection select() { - // Note: the fact we return Select.Selection as return type is on purpose. - return new Select.SelectionOrAlias(); - } - - /** - * Starts building a new {@code INSERT} query. - * - * @param table the name of the table in which to insert. - * @return an in-construction {@code INSERT} query. - */ - public static Insert insertInto(String table) { - return new Insert(null, table); - } - - /** - * Starts building a new {@code INSERT} query. - * - * @param keyspace the name of the keyspace to use. - * @param table the name of the table to insert into. - * @return an in-construction {@code INSERT} query. - */ - public static Insert insertInto(String keyspace, String table) { - return new Insert(keyspace, table); - } - - /** - * Starts building a new {@code INSERT} query. - * - * @param table the name of the table to insert into. - * @return an in-construction {@code INSERT} query. - */ - public static Insert insertInto(TableMetadata table) { - return new Insert(table); - } - - /** - * Starts building a new {@code UPDATE} query. - * - * @param table the name of the table to update. - * @return an in-construction {@code UPDATE} query (at least a {@code SET} and a {@code WHERE} - * clause needs to be provided to complete the query). - */ - public static Update update(String table) { - return new Update(null, table); - } - - /** - * Starts building a new {@code UPDATE} query. - * - * @param keyspace the name of the keyspace to use. - * @param table the name of the table to update. - * @return an in-construction {@code UPDATE} query (at least a {@code SET} and a {@code WHERE} - * clause needs to be provided to complete the query). - */ - public static Update update(String keyspace, String table) { - return new Update(keyspace, table); - } - - /** - * Starts building a new {@code UPDATE} query. - * - * @param table the name of the table to update. - * @return an in-construction {@code UPDATE} query (at least a {@code SET} and a {@code WHERE} - * clause needs to be provided to complete the query). - */ - public static Update update(TableMetadata table) { - return new Update(table); - } - - /** - * Starts building a new {@code DELETE} query that deletes the provided names. - * - * @param columns the columns names that should be deleted by the query. - * @return an in-construction {@code DELETE} query (At least a {@code FROM} and a {@code WHERE} - * clause needs to be provided to complete the query). - */ - public static Delete.Builder delete(String... columns) { - return new Delete.Builder(columns); - } - - /** - * Starts building a new {@code DELETE} query. - * - * @return an in-construction {@code DELETE} query (you will need to provide a column selection - * and at least a {@code FROM} and a {@code WHERE} clause to complete the query). - */ - public static Delete.Selection delete() { - return new Delete.Selection(); - } - - /** - * Builds a new {@code BATCH} query on the provided statements. - * - *

This method will build a logged batch (this is the default in CQL3). To create unlogged - * batches, use {@link #unloggedBatch}. Also note that for convenience, if the provided statements - * are counter statements, this method will create a {@code COUNTER} batch even though COUNTER - * batches are never logged (so for counters, using this method is effectively equivalent to using - * {@link #unloggedBatch}). - * - * @param statements the statements to batch. - * @return a new {@code RegularStatement} that batch {@code statements}. - */ - public static Batch batch(RegularStatement... statements) { - return new Batch(statements, true); - } - - /** - * Builds a new {@code UNLOGGED BATCH} query on the provided statements. - * - *

Compared to logged batches (the default), unlogged batch don't use the distributed batch log - * server side and as such are not guaranteed to be atomic. In other words, if an unlogged batch - * timeout, some of the batched statements may have been persisted while some have not. Unlogged - * batch will however be slightly faster than logged batch. - * - *

If the statements added to the batch are counter statements, the resulting batch will be a - * {@code COUNTER} one. - * - * @param statements the statements to batch. - * @return a new {@code RegularStatement} that batch {@code statements} without using the batch - * log. - */ - public static Batch unloggedBatch(RegularStatement... statements) { - return new Batch(statements, false); - } - - /** - * Creates a new {@code TRUNCATE} query. - * - * @param table the name of the table to truncate. - * @return the truncation query. - */ - public static Truncate truncate(String table) { - return new Truncate(null, table); - } - - /** - * Creates a new {@code TRUNCATE} query. - * - * @param keyspace the name of the keyspace to use. - * @param table the name of the table to truncate. - * @return the truncation query. - */ - public static Truncate truncate(String keyspace, String table) { - return new Truncate(keyspace, table); - } - - /** - * Creates a new {@code TRUNCATE} query. - * - * @param table the table to truncate. - * @return the truncation query. - */ - public static Truncate truncate(TableMetadata table) { - return new Truncate(table); - } - - /** - * Quotes a column name to make it case sensitive. - * - * @param columnName the column name to quote. - * @return the quoted column name. - * @see Metadata#quote(String) - */ - public static String quote(String columnName) { - return Metadata.quote(columnName); - } - - /** - * The token of a column name. - * - * @param columnName the column name to take the token of. - * @return {@code "token(" + columnName + ")"}. - */ - public static String token(String columnName) { - StringBuilder sb = new StringBuilder(); - sb.append("token("); - Utils.appendName(columnName, sb); - sb.append(')'); - return sb.toString(); - } - - /** - * The token of column names. - * - *

This variant is most useful when the partition key is composite. - * - * @param columnNames the column names to take the token of. - * @return a string representing the token of the provided column names. - */ - public static String token(String... columnNames) { - StringBuilder sb = new StringBuilder(); - sb.append("token("); - Utils.joinAndAppendNames(sb, null, Arrays.asList(columnNames)); - sb.append(')'); - return sb.toString(); - } - - /** - * Returns a generic {@code token} function call. - * - * @param values the arguments of the {@code token} function. - * @return {@code token} function call. - */ - public static Object token(Object... values) { - return new Utils.FCall("token", values); - } - - /** - * Creates an "equal" {@code WHERE} clause stating the provided column must be equal to the - * provided value. - * - * @param name the column name - * @param value the value - * @return the corresponding where clause. - */ - public static Clause eq(String name, Object value) { - return new Clause.SimpleClause(name, "=", value); - } - - /** - * Creates an "equal" {@code WHERE} clause for a group of clustering columns. - * - *

For instance, {@code eq(Arrays.asList("a", "b"), Arrays.asList(2, "test"))} will generate - * the CQL {@code WHERE} clause {@code (a, b) = (2, 'test') }. - * - *

Please note that this variant is only supported starting with Cassandra 2.0.6. - * - * @param names the column names - * @param values the values - * @return the corresponding where clause. - * @throws IllegalArgumentException if {@code names.size() != values.size()}. - */ - public static Clause eq(Iterable names, Iterable values) { - return new Clause.CompoundClause(names, "=", values); - } - - /** - * Creates a "not equal" {@code WHERE} clause stating the provided column must be different from - * the provided value. - * - * @param name the column name - * @param value the value - * @return the corresponding where clause. - */ - public static Clause ne(String name, Object value) { - return new Clause.SimpleClause(name, "!=", value); - } - - /** - * Creates an "not equal" {@code WHERE} clause for a group of clustering columns. - * - *

For instance, {@code eq(Arrays.asList("a", "b"), Arrays.asList(2, "test"))} will generate - * the CQL {@code WHERE} clause {@code (a, b) != (2, 'test') }. - * - * @param names the column names - * @param values the values - * @return the corresponding where clause. - * @throws IllegalArgumentException if {@code names.size() != values.size()}. - */ - public static Clause ne(Iterable names, Iterable values) { - return new Clause.CompoundClause(names, "!=", values); - } - - /** - * Creates an "IS NOT NULL" {@code WHERE} clause for the provided column. - * - * @param name the column name - * @return the corresponding where clause. - */ - public static Clause notNull(String name) { - return new Clause.IsNotNullClause(name); - } - - /** - * Creates a "like" {@code WHERE} clause stating that the provided column must be equal to the - * provided value. - * - * @param name the column name. - * @param value the value. - * @return the corresponding where clause. - */ - public static Clause like(String name, Object value) { - return new Clause.SimpleClause(name, " LIKE ", value); - } - - /** - * Create an "in" {@code WHERE} clause stating the provided column must be equal to one of the - * provided values. - * - * @param name the column name - * @param values the values - * @return the corresponding where clause. - */ - public static Clause in(String name, Object... values) { - return new Clause.InClause(name, Arrays.asList(values)); - } - - /** - * Create an "in" {@code WHERE} clause stating the provided column must be equal to one of the - * provided values. - * - * @param name the column name - * @param values the values - * @return the corresponding where clause. - */ - public static Clause in(String name, Iterable values) { - return new Clause.InClause(name, values); - } - - /** - * Creates an "in" {@code WHERE} clause for a group of clustering columns (a.k.a. "multi-column IN - * restriction"). - * - *

For instance, {@code in(Arrays.asList("a", "b"), Arrays.asList(Arrays.asList(1, "foo"), - * Arrays.asList(2, "bar")))} will generate the CQL {@code WHERE} clause {@code (a, b) IN ((1, - * 'foo'), (2, 'bar'))}. - * - *

Each element in {@code values} must be either an {@link Iterable iterable} containing - * exactly as many values as there are columns to match in {@code names}, or a {@link - * #bindMarker() bind marker} – in which case, that marker is to be considered as a placeholder - * for one whole tuple of values to match. - * - *

Please note that this variant is only supported starting with Cassandra 2.0.9. - * - * @param names the column names - * @param values the values - * @return the corresponding where clause. - * @throws IllegalArgumentException if the size of any tuple in {@code values} is not equal to - * {@code names.size()}, or if {@code values} contains elements that are neither {@link List - * lists} nor {@link #bindMarker() bind markers}. - */ - public static Clause in(Iterable names, Iterable values) { - return new Clause.CompoundInClause(names, values); - } - - /** - * Creates a "contains" {@code WHERE} clause stating the provided column must contain the value - * provided. - * - * @param name the column name - * @param value the value - * @return the corresponding where clause. - */ - public static Clause contains(String name, Object value) { - return new Clause.ContainsClause(name, value); - } - - /** - * Creates a "contains key" {@code WHERE} clause stating the provided column must contain the key - * provided. - * - * @param name the column name - * @param key the key - * @return the corresponding where clause. - */ - public static Clause containsKey(String name, Object key) { - return new Clause.ContainsKeyClause(name, key); - } - - /** - * Creates a "lesser than" {@code WHERE} clause stating the provided column must be less than the - * provided value. - * - * @param name the column name - * @param value the value - * @return the corresponding where clause. - */ - public static Clause lt(String name, Object value) { - return new Clause.SimpleClause(name, "<", value); - } - - /** - * Creates a "lesser than" {@code WHERE} clause for a group of clustering columns. - * - *

For instance, {@code lt(Arrays.asList("a", "b"), Arrays.asList(2, "test"))} will generate - * the CQL {@code WHERE} clause {@code (a, b) < (2, 'test') }. - * - *

Please note that this variant is only supported starting with Cassandra 2.0.6. - * - * @param names the column names - * @param values the values - * @return the corresponding where clause. - * @throws IllegalArgumentException if {@code names.size() != values.size()}. - */ - public static Clause lt(Iterable names, Iterable values) { - return new Clause.CompoundClause(names, "<", values); - } - - /** - * Creates a "lesser than or equal" {@code WHERE} clause stating the provided column must be - * lesser than or equal to the provided value. - * - * @param name the column name - * @param value the value - * @return the corresponding where clause. - */ - public static Clause lte(String name, Object value) { - return new Clause.SimpleClause(name, "<=", value); - } - - /** - * Creates a "lesser than or equal" {@code WHERE} clause for a group of clustering columns. - * - *

For instance, {@code lte(Arrays.asList("a", "b"), Arrays.asList(2, "test"))} will generate - * the CQL {@code WHERE} clause {@code (a, b) <e; (2, 'test') }. - * - *

Please note that this variant is only supported starting with Cassandra 2.0.6. - * - * @param names the column names - * @param values the values - * @return the corresponding where clause. - * @throws IllegalArgumentException if {@code names.size() != values.size()}. - */ - public static Clause lte(Iterable names, Iterable values) { - return new Clause.CompoundClause(names, "<=", values); - } - - /** - * Creates a "greater than" {@code WHERE} clause stating the provided column must be greater to - * the provided value. - * - * @param name the column name - * @param value the value - * @return the corresponding where clause. - */ - public static Clause gt(String name, Object value) { - return new Clause.SimpleClause(name, ">", value); - } - - /** - * Creates a "greater than" {@code WHERE} clause for a group of clustering columns. - * - *

For instance, {@code gt(Arrays.asList("a", "b"), Arrays.asList(2, "test"))} will generate - * the CQL {@code WHERE} clause {@code (a, b) > (2, 'test') }. - * - *

Please note that this variant is only supported starting with Cassandra 2.0.6. - * - * @param names the column names - * @param values the values - * @return the corresponding where clause. - * @throws IllegalArgumentException if {@code names.size() != values.size()}. - */ - public static Clause gt(Iterable names, Iterable values) { - return new Clause.CompoundClause(names, ">", values); - } - - /** - * Creates a "greater than or equal" {@code WHERE} clause stating the provided column must be - * greater than or equal to the provided value. - * - * @param name the column name - * @param value the value - * @return the corresponding where clause. - */ - public static Clause gte(String name, Object value) { - return new Clause.SimpleClause(name, ">=", value); - } - - /** - * Creates a "greater than or equal" {@code WHERE} clause for a group of clustering columns. - * - *

For instance, {@code gte(Arrays.asList("a", "b"), Arrays.asList(2, "test"))} will generate - * the CQL {@code WHERE} clause {@code (a, b) >e; (2, 'test') }. - * - *

Please note that this variant is only supported starting with Cassandra 2.0.6. - * - * @param names the column names - * @param values the values - * @return the corresponding where clause. - * @throws IllegalArgumentException if {@code names.size() != values.size()}. - */ - public static Clause gte(Iterable names, Iterable values) { - return new Clause.CompoundClause(names, ">=", values); - } - - /** - * Ascending ordering for the provided column. - * - * @param columnName the column name - * @return the corresponding ordering - */ - public static Ordering asc(String columnName) { - return new Ordering(columnName, false); - } - - /** - * Descending ordering for the provided column. - * - * @param columnName the column name - * @return the corresponding ordering - */ - public static Ordering desc(String columnName) { - return new Ordering(columnName, true); - } - - /** - * Option to set the timestamp for a modification query (insert, update or delete). - * - * @param timestamp the timestamp (in microseconds) to use. - * @return the corresponding option - * @throws IllegalArgumentException if {@code timestamp < 0}. - */ - public static Using timestamp(long timestamp) { - if (timestamp < 0) throw new IllegalArgumentException("Invalid timestamp, must be positive"); - - return new Using.WithValue("TIMESTAMP", timestamp); - } - - /** - * Option to prepare the timestamp (in microseconds) for a modification query (insert, update or - * delete). - * - * @param marker bind marker to use for the timestamp. - * @return the corresponding option. - */ - public static Using timestamp(BindMarker marker) { - return new Using.WithMarker("TIMESTAMP", marker); - } - - /** - * Option to set the ttl for a modification query (insert, update or delete). - * - * @param ttl the ttl (in seconds) to use. - * @return the corresponding option - * @throws IllegalArgumentException if {@code ttl < 0}. - */ - public static Using ttl(int ttl) { - if (ttl < 0) throw new IllegalArgumentException("Invalid ttl, must be positive"); - - return new Using.WithValue("TTL", ttl); - } - - /** - * Option to prepare the ttl (in seconds) for a modification query (insert, update or delete). - * - * @param marker bind marker to use for the ttl. - * @return the corresponding option - */ - public static Using ttl(BindMarker marker) { - return new Using.WithMarker("TTL", marker); - } - - /** - * Simple "set" assignment of a value to a column. - * - *

This will generate: - * - *

-   * name = value
-   * 
- * - * The column name will only be quoted if it contains special characters, as in: - * - *
-   * "a name that contains spaces" = value
-   * 
- * - * Otherwise, if you want to force case sensitivity, use {@link #quote(String)}: - * - *
-   * set(quote("aCaseSensitiveName"), value)
-   * 
- * - * This method won't work to set UDT fields; use {@link #set(Object, Object)} with a {@link - * #path(String...) path} instead: - * - *
-   * set(path("udt", "field"), value)
-   * 
- * - * @param name the column name - * @param value the value to assign - * @return the correspond assignment (to use in an update query) - */ - public static Assignment set(String name, Object value) { - return new Assignment.SetAssignment(name, value); - } - - /** - * Advanced "set" assignment of a value to a column or a {@link com.datastax.driver.core.UserType - * UDT} field. - * - *

This method is seldom preferable to {@link #set(String, Object)}; it is only useful: - * - *

    - *
  • when assigning values to individual fields of a UDT (see {@link #path(String...)}): - *
    -   * set(path("udt", "field"), value)
    -   * 
    - *
  • if you wish to pass a "raw" string that will get appended as-is to the query (see {@link - * #raw(String)}). There is no practical usage for this the time of writing, but it will - * serve as a workaround if new features are added to Cassandra and you're using a older - * driver version that is not yet aware of them: - *
    -   * set(raw("some custom string"), value)
    -   * 
    - *
- * - * If the runtime type of {@code name} is {@code String}, this method is equivalent to {@link - * #set(String, Object)}. - * - * @param name the column or UDT field name - * @param value the value to assign - * @return the correspond assignment (to use in an update query) - */ - public static Assignment set(Object name, Object value) { - return new Assignment.SetAssignment(name, value); - } - - /** - * Incrementation of a counter column. - * - *

This will generate: {@code name = name + 1}. - * - * @param name the column name to increment - * @return the correspond assignment (to use in an update query) - */ - public static Assignment incr(String name) { - return incr(name, 1L); - } - - /** - * Incrementation of a counter column by a provided value. - * - *

This will generate: {@code name = name + value}. - * - * @param name the column name to increment - * @param value the value by which to increment - * @return the correspond assignment (to use in an update query) - */ - public static Assignment incr(String name, long value) { - return new Assignment.CounterAssignment(name, value, true); - } - - /** - * Incrementation of a counter column by a provided value. - * - *

This will generate: {@code name = name + value}. - * - * @param name the column name to increment - * @param value a bind marker representing the value by which to increment - * @return the correspond assignment (to use in an update query) - */ - public static Assignment incr(String name, BindMarker value) { - return new Assignment.CounterAssignment(name, value, true); - } - - /** - * Decrementation of a counter column. - * - *

This will generate: {@code name = name - 1}. - * - * @param name the column name to decrement - * @return the correspond assignment (to use in an update query) - */ - public static Assignment decr(String name) { - return decr(name, 1L); - } - - /** - * Decrementation of a counter column by a provided value. - * - *

This will generate: {@code name = name - value}. - * - * @param name the column name to decrement - * @param value the value by which to decrement - * @return the correspond assignment (to use in an update query) - */ - public static Assignment decr(String name, long value) { - return new Assignment.CounterAssignment(name, value, false); - } - - /** - * Decrementation of a counter column by a provided value. - * - *

This will generate: {@code name = name - value}. - * - * @param name the column name to decrement - * @param value a bind marker representing the value by which to decrement - * @return the correspond assignment (to use in an update query) - */ - public static Assignment decr(String name, BindMarker value) { - return new Assignment.CounterAssignment(name, value, false); - } - - /** - * Prepend a value to a list column. - * - *

This will generate: {@code name = [ value ] + name}. - * - * @param name the column name (must be of type list). - * @param value the value to prepend. Using a BindMarker here is not supported. To use a - * BindMarker use {@code QueryBuilder#prependAll} with a singleton list. - * @return the correspond assignment (to use in an update query) - */ - public static Assignment prepend(String name, Object value) { - if (value instanceof BindMarker) { - throw new InvalidQueryException( - "binding a value in prepend() is not supported, use prependAll() and bind a singleton list"); - } - return prependAll(name, Collections.singletonList(value)); - } - - /** - * Prepend a list of values to a list column. - * - *

This will generate: {@code name = list + name}. - * - * @param name the column name (must be of type list). - * @param list the list of values to prepend. - * @return the correspond assignment (to use in an update query) - */ - public static Assignment prependAll(String name, List list) { - return new Assignment.ListPrependAssignment(name, list); - } - - /** - * Prepend a list of values to a list column. - * - *

This will generate: {@code name = list + name}. - * - * @param name the column name (must be of type list). - * @param list a bind marker representing the list of values to prepend. - * @return the correspond assignment (to use in an update query) - */ - public static Assignment prependAll(String name, BindMarker list) { - return new Assignment.ListPrependAssignment(name, list); - } - - /** - * Append a value to a list column. - * - *

This will generate: {@code name = name + [value]}. - * - * @param name the column name (must be of type list). - * @param value the value to append. Using a BindMarker here is not supported. To use a BindMarker - * use {@code QueryBuilder#appendAll} with a singleton list. - * @return the correspond assignment (to use in an update query) - */ - public static Assignment append(String name, Object value) { - if (value instanceof BindMarker) { - throw new InvalidQueryException( - "Binding a value in append() is not supported, use appendAll() and bind a singleton list"); - } - return appendAll(name, Collections.singletonList(value)); - } - - /** - * Append a list of values to a list column. - * - *

This will generate: {@code name = name + list}. - * - * @param name the column name (must be of type list). - * @param list the list of values to append - * @return the correspond assignment (to use in an update query) - */ - public static Assignment appendAll(String name, List list) { - return new Assignment.CollectionAssignment(name, list, true, false); - } - - /** - * Append a list of values to a list column. - * - *

This will generate: {@code name = name + list}. - * - * @param name the column name (must be of type list). - * @param list a bind marker representing the list of values to append - * @return the correspond assignment (to use in an update query) - */ - public static Assignment appendAll(String name, BindMarker list) { - return new Assignment.CollectionAssignment(name, list, true, false); - } - - /** - * Discard a value from a list column. - * - *

This will generate: {@code name = name - [value]}. - * - * @param name the column name (must be of type list). - * @param value the value to discard. Using a BindMarker here is not supported. To use a - * BindMarker use {@code QueryBuilder#discardAll} with a singleton list. - * @return the correspond assignment (to use in an update query) - */ - public static Assignment discard(String name, Object value) { - if (value instanceof BindMarker) { - throw new InvalidQueryException( - "Binding a value in discard() is not supported, use discardAll() and bind a singleton list"); - } - return discardAll(name, Collections.singletonList(value)); - } - - /** - * Discard a list of values to a list column. - * - *

This will generate: {@code name = name - list}. - * - * @param name the column name (must be of type list). - * @param list the list of values to discard - * @return the correspond assignment (to use in an update query) - */ - public static Assignment discardAll(String name, List list) { - return new Assignment.CollectionAssignment(name, list, false); - } - - /** - * Discard a list of values to a list column. - * - *

This will generate: {@code name = name - list}. - * - * @param name the column name (must be of type list). - * @param list a bind marker representing the list of values to discard - * @return the correspond assignment (to use in an update query) - */ - public static Assignment discardAll(String name, BindMarker list) { - return new Assignment.CollectionAssignment(name, list, false); - } - - /** - * Sets a list column value by index. - * - *

This will generate: {@code name[idx] = value}. - * - * @param name the column name (must be of type list). - * @param idx the index to set - * @param value the value to set - * @return the correspond assignment (to use in an update query) - */ - public static Assignment setIdx(String name, int idx, Object value) { - return new Assignment.ListSetIdxAssignment(name, idx, value); - } - - /** - * Adds a value to a set column. - * - *

This will generate: {@code name = name + {value}}. - * - * @param name the column name (must be of type set). - * @param value the value to add. Using a BindMarker here is not supported. To use a BindMarker - * use {@code QueryBuilder#addAll} with a singleton set. - * @return the correspond assignment (to use in an update query) - */ - public static Assignment add(String name, Object value) { - if (value instanceof BindMarker) { - throw new InvalidQueryException( - "Binding a value in add() is not supported, use addAll() and bind a singleton list"); - } - return addAll(name, Collections.singleton(value)); - } - - /** - * Adds a set of values to a set column. - * - *

This will generate: {@code name = name + set}. - * - * @param name the column name (must be of type set). - * @param set the set of values to append - * @return the correspond assignment (to use in an update query) - */ - public static Assignment addAll(String name, Set set) { - return new Assignment.CollectionAssignment(name, set, true); - } - - /** - * Adds a set of values to a set column. - * - *

This will generate: {@code name = name + set}. - * - * @param name the column name (must be of type set). - * @param set a bind marker representing the set of values to append - * @return the correspond assignment (to use in an update query) - */ - public static Assignment addAll(String name, BindMarker set) { - return new Assignment.CollectionAssignment(name, set, true); - } - - /** - * Remove a value from a set column. - * - *

This will generate: {@code name = name - {value}}. - * - * @param name the column name (must be of type set). - * @param value the value to remove. Using a BindMarker here is not supported. To use a BindMarker - * use {@code QueryBuilder#removeAll} with a singleton set. - * @return the correspond assignment (to use in an update query) - */ - public static Assignment remove(String name, Object value) { - if (value instanceof BindMarker) { - throw new InvalidQueryException( - "Binding a value in remove() is not supported, use removeAll() and bind a singleton set"); - } - return removeAll(name, Collections.singleton(value)); - } - - /** - * Remove a set of values from a set column. - * - *

This will generate: {@code name = name - set}. - * - * @param name the column name (must be of type set). - * @param set the set of values to remove - * @return the correspond assignment (to use in an update query) - */ - public static Assignment removeAll(String name, Set set) { - return new Assignment.CollectionAssignment(name, set, false); - } - - /** - * Remove a set of values from a set column. - * - *

This will generate: {@code name = name - set}. - * - * @param name the column name (must be of type set). - * @param set a bind marker representing the set of values to remove - * @return the correspond assignment (to use in an update query) - */ - public static Assignment removeAll(String name, BindMarker set) { - return new Assignment.CollectionAssignment(name, set, false); - } - - /** - * Puts a new key/value pair to a map column. - * - *

This will generate: {@code name[key] = value}. - * - * @param name the column name (must be of type map). - * @param key the key to put - * @param value the value to put - * @return the correspond assignment (to use in an update query) - */ - public static Assignment put(String name, Object key, Object value) { - return new Assignment.MapPutAssignment(name, key, value); - } - - /** - * Puts a map of new key/value pairs to a map column. - * - *

This will generate: {@code name = name + map}. - * - * @param name the column name (must be of type map). - * @param map the map of key/value pairs to put - * @return the correspond assignment (to use in an update query) - */ - public static Assignment putAll(String name, Map map) { - return new Assignment.CollectionAssignment(name, map, true); - } - - /** - * Puts a map of new key/value pairs to a map column. - * - *

This will generate: {@code name = name + map}. - * - * @param name the column name (must be of type map). - * @param map a bind marker representing the map of key/value pairs to put - * @return the correspond assignment (to use in an update query) - */ - public static Assignment putAll(String name, BindMarker map) { - return new Assignment.CollectionAssignment(name, map, true); - } - - /** - * An object representing an anonymous bind marker (a question mark). - * - *

This can be used wherever a value is expected. For instance, one can do: - * - *

{@code
-   * Insert i = QueryBuilder.insertInto("test").value("k", 0)
-   *                                           .value("c", QueryBuilder.bindMarker());
-   * PreparedStatement p = session.prepare(i.toString());
-   * }
- * - * @return a new bind marker. - */ - public static BindMarker bindMarker() { - return BindMarker.ANONYMOUS; - } - - /** - * An object representing a named bind marker. - * - *

This can be used wherever a value is expected. For instance, one can do: - * - *

{@code
-   * Insert i = QueryBuilder.insertInto("test").value("k", 0)
-   *                                           .value("c", QueryBuilder.bindMarker("c_val"));
-   * PreparedStatement p = session.prepare(i.toString());
-   * }
- * - *

Please note that named bind makers are only supported starting with Cassandra 2.0.1. - * - * @param name the name for the bind marker. - * @return an object representing a bind marker named {@code name}. - */ - public static BindMarker bindMarker(String name) { - return new BindMarker(name); - } - - /** - * Protects a value from any interpretation by the query builder. - * - *

The following table exemplify the behavior of this function: - * - * - * - * - * - * - * - * - * - *
Examples of use
CodeResulting query string
{@code select().from("t").where(eq("c", "C'est la vie!")); }{@code "SELECT * FROM t WHERE c='C''est la vie!';"}
{@code select().from("t").where(eq("c", raw("C'est la vie!"))); }{@code "SELECT * FROM t WHERE c=C'est la vie!;"}
{@code select().from("t").where(eq("c", raw("'C'est la vie!'"))); }{@code "SELECT * FROM t WHERE c='C'est la vie!';"}
{@code select().from("t").where(eq("c", "now()")); }{@code "SELECT * FROM t WHERE c='now()';"}
{@code select().from("t").where(eq("c", raw("now()"))); }{@code "SELECT * FROM t WHERE c=now();"}
- * - * Note: the 2nd and 3rd examples in this table are not a valid CQL3 queries. - * - *

The use of that method is generally discouraged since it lead to security risks. However, if - * you know what you are doing, it allows to escape the interpretations done by the QueryBuilder. - * - * @param str the raw value to use as a string - * @return the value but protected from being interpreted/escaped by the query builder. - */ - public static Object raw(String str) { - return new Utils.RawString(str); - } - - /** - * Creates a function call. - * - * @param name the name of the function to call. - * @param parameters the parameters for the function. - * @return the function call. - */ - public static Object fcall(String name, Object... parameters) { - return new Utils.FCall(name, parameters); - } - - /** - * Creates a Cast of a column using the given dataType. - * - * @param column the column to cast. - * @param dataType the data type to cast to. - * @return the casted column. - */ - public static Object cast(Object column, DataType dataType) { - return new Utils.Cast(column, dataType); - } - - /** - * Creates a {@code now()} function call. - * - * @return the function call. - */ - public static Object now() { - return new Utils.FCall("now"); - } - - /** - * Creates a {@code uuid()} function call. - * - * @return the function call. - */ - public static Object uuid() { - return new Utils.FCall("uuid"); - } - - /** - * Declares that the name in argument should be treated as a column name. - * - *

This mainly meant for use with {@link Select.Selection#fcall} when a function should apply - * to a column name, not a string value. - * - * @param name the name of the column. - * @return the name as a column name. - */ - public static Object column(String name) { - return new Utils.CName(name); - } - - /** - * Creates a path composed of the given path {@code segments}. - * - *

All provided path segments will be concatenated together with dots. If any segment contains - * an identifier that needs quoting, caller code is expected to call {@link #quote(String)} prior - * to invoking this method. - * - *

This method is currently only useful when accessing individual fields of a {@link - * com.datastax.driver.core.UserType user-defined type} (UDT), which is only possible since - * CASSANDRA-7423. - * - *

Note that currently nested UDT fields are not supported and will be rejected by the server - * as a {@link com.datastax.driver.core.exceptions.SyntaxError syntax error}. - * - * @param segments the segments of the path to create. - * @return the segments concatenated as a single path. - * @see CASSANDRA-7423 - */ - public static Object path(String... segments) { - return new Utils.Path(segments); - } - - /** - * Creates a {@code fromJson()} function call. - * - *

Support for JSON functions has been added in Cassandra 2.2. The {@code fromJson()} function - * is similar to {@code INSERT JSON} statements, but applies to a single column value instead of - * the entire row, and converts a JSON object into the normal Cassandra column value. - * - *

It may be used in {@code INSERT} and {@code UPDATE} statements, but NOT in the selection - * clause of a {@code SELECT} statement. - * - *

The provided object can be of the following types: - * - *

    - *
  1. A raw string. In this case, it will be appended to the query string as is. It - * should NOT be surrounded by single quotes. Its format should generally match - * that returned by a {@code SELECT JSON} statement on the same table. Note that it is not - * possible to insert function calls nor bind markers in a JSON string. - *
  2. A {@link QueryBuilder#bindMarker() bind marker}. In this case, the statement is meant to - * be prepared and no JSON string will be appended to the query string, only a bind marker - * for the whole JSON parameter. - *
  3. Any object that can be serialized to JSON. Such objects can be used provided that a - * matching {@link com.datastax.driver.core.TypeCodec codec} is registered with the {@link - * com.datastax.driver.core.CodecRegistry CodecRegistry} in use. This allows the usage of - * JSON libraries, such as the Java API for - * JSON processing, the popular Jackson library, or Google's Gson library, for instance. - *
- * - *

When passing raw strings to this method, the following rules apply: - * - *

    - *
  1. String values should be enclosed in double quotes. - *
  2. Double quotes appearing inside strings should be escaped with a backslash, but single - * quotes should be escaped in the CQL manner, i.e. by another single quote. For example, - * the column value {@code foo"'bar} should be inserted in the JSON string as {@code - * "foo\"''bar"}. - *
- * - * @param json the JSON string, or a bind marker, or a JSON object handled by a specific {@link - * com.datastax.driver.core.TypeCodec codec}. - * @return the function call. - * @see JSON Support for CQL - * @see JSON - * Support in Cassandra 2.2 - */ - public static Object fromJson(Object json) { - return fcall("fromJson", json); - } - - /** - * Creates a {@code toJson()} function call. This is a shortcut for {@code fcall("toJson", - * QueryBuilder.column(name))}. - * - *

Support for JSON functions has been added in Cassandra 2.2. The {@code toJson()} function is - * similar to {@code SELECT JSON} statements, but applies to a single column value instead of the - * entire row, and produces a JSON-encoded string representing the normal Cassandra column value. - * - *

It may only be used in the selection clause of a {@code SELECT} statement. - * - * @param column the column to retrieve JSON from. - * @return the function call. - * @see JSON Support for CQL - * @see JSON - * Support in Cassandra 2.2 - */ - public static Object toJson(Object column) { - // consider a String literal as a column name for user convenience, - // as CQL literals are not allowed here. - if (column instanceof String) column = column(((String) column)); - return new Utils.FCall("toJson", column); - } - - /** - * Creates an alias for a given column. - * - *

This is most useful when used with the method {@link #select(Object...)}. - * - * @param column The column to create an alias for. - * @param alias The column alias. - * @return a column alias. - */ - public static Object alias(Object column, String alias) { - return new Utils.Alias(column, alias); - } - - /** - * Creates a {@code count(x)} built-in function call. - * - * @return the function call. - */ - public static Object count(Object column) { - // consider a String literal as a column name for user convenience, - // as CQL literals are not allowed here. - if (column instanceof String) column = column(((String) column)); - return new Utils.FCall("count", column); - } - - /** - * Creates a {@code max(x)} built-in function call. - * - * @return the function call. - */ - public static Object max(Object column) { - // consider a String literal as a column name for user convenience, - // as CQL literals are not allowed here. - if (column instanceof String) column = column(((String) column)); - return new Utils.FCall("max", column); - } - - /** - * Creates a {@code min(x)} built-in function call. - * - * @return the function call. - */ - public static Object min(Object column) { - // consider a String literal as a column name for user convenience, - // as CQL literals are not allowed here. - if (column instanceof String) column = column(((String) column)); - return new Utils.FCall("min", column); - } - - /** - * Creates a {@code sum(x)} built-in function call. - * - * @return the function call. - */ - public static Object sum(Object column) { - // consider a String literal as a column name for user convenience, - // as CQL literals are not allowed here. - if (column instanceof String) column = column(((String) column)); - return new Utils.FCall("sum", column); - } - - /** - * Creates an {@code avg(x)} built-in function call. - * - * @return the function call. - */ - public static Object avg(Object column) { - // consider a String literal as a column name for user convenience, - // as CQL literals are not allowed here. - if (column instanceof String) column = column(((String) column)); - return new Utils.FCall("avg", column); - } -} diff --git a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Select.java b/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Select.java deleted file mode 100644 index 1974e5fca30..00000000000 --- a/driver-core/src/main/java/com/datastax/driver/core/querybuilder/Select.java +++ /dev/null @@ -1,883 +0,0 @@ -/* - * Copyright DataStax, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.datastax.driver.core.querybuilder; - -import com.datastax.driver.core.AbstractTableMetadata; -import com.datastax.driver.core.CodecRegistry; -import com.datastax.driver.core.ColumnMetadata; -import com.datastax.driver.core.DataType; -import com.datastax.driver.core.MaterializedViewMetadata; -import com.datastax.driver.core.Metadata; -import com.datastax.driver.core.TableMetadata; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -/** A built SELECT statement. */ -public class Select extends BuiltStatement { - - private static final List COUNT_ALL = - Collections.singletonList(new Utils.FCall("count", new Utils.RawString("*"))); - - private final String table; - private final boolean isDistinct; - private final boolean isJson; - private final List columnNames; - private final Where where; - private List orderings; - private List groupByColumnNames; - private Object limit; - private Object perPartitionLimit; - private boolean allowFiltering; - - Select( - String keyspace, String table, List columnNames, boolean isDistinct, boolean isJson) { - this(keyspace, table, null, null, columnNames, isDistinct, isJson); - } - - Select( - AbstractTableMetadata table, List columnNames, boolean isDistinct, boolean isJson) { - this( - Metadata.quoteIfNecessary(table.getKeyspace().getName()), - Metadata.quoteIfNecessary(table.getName()), - Arrays.asList(new Object[table.getPartitionKey().size()]), - table.getPartitionKey(), - columnNames, - isDistinct, - isJson); - } - - Select( - String keyspace, - String table, - List routingKeyValues, - List partitionKey, - List columnNames, - boolean isDistinct, - boolean isJson) { - super(keyspace, partitionKey, routingKeyValues); - this.table = table; - this.columnNames = columnNames; - this.isDistinct = isDistinct; - this.isJson = isJson; - this.where = new Where(this); - } - - @Override - StringBuilder buildQueryString(List variables, CodecRegistry codecRegistry) { - StringBuilder builder = new StringBuilder(); - - builder.append("SELECT "); - - if (isJson) builder.append("JSON "); - - if (isDistinct) builder.append("DISTINCT "); - - if (columnNames == null) { - builder.append('*'); - } else { - Utils.joinAndAppendNames(builder, codecRegistry, columnNames); - } - builder.append(" FROM "); - if (keyspace != null) Utils.appendName(keyspace, builder).append('.'); - Utils.appendName(table, builder); - - if (!where.clauses.isEmpty()) { - builder.append(" WHERE "); - Utils.joinAndAppend(builder, codecRegistry, " AND ", where.clauses, variables); - } - - if (groupByColumnNames != null) { - builder.append(" GROUP BY "); - Utils.joinAndAppendNames(builder, codecRegistry, groupByColumnNames); - } - - if (orderings != null) { - builder.append(" ORDER BY "); - Utils.joinAndAppend(builder, codecRegistry, ",", orderings, variables); - } - - if (perPartitionLimit != null) { - builder.append(" PER PARTITION LIMIT ").append(perPartitionLimit); - } - - if (limit != null) { - builder.append(" LIMIT ").append(limit); - } - - if (allowFiltering) { - builder.append(" ALLOW FILTERING"); - } - - return builder; - } - - /** - * Adds a {@code WHERE} clause to this statement. - * - *

This is a shorter/more readable version for {@code where().and(clause)}. - * - * @param clause the clause to add. - * @return the where clause of this query to which more clause can be added. - */ - public Where where(Clause clause) { - return where.and(clause); - } - - /** - * Returns a {@code WHERE} statement for this query without adding clause. - * - * @return the where clause of this query to which more clause can be added. - */ - public Where where() { - return where; - } - - /** - * Adds an {@code ORDER BY} clause to this statement. - * - * @param orderings the orderings to define for this query. - * @return this statement. - * @throws IllegalStateException if an {@code ORDER BY} clause has already been provided. - */ - public Select orderBy(Ordering... orderings) { - if (this.orderings != null) - throw new IllegalStateException("An ORDER BY clause has already been provided"); - - if (orderings.length == 0) - throw new IllegalArgumentException( - "Invalid ORDER BY argument, the orderings must not be empty."); - - this.orderings = Arrays.asList(orderings); - for (Ordering ordering : orderings) checkForBindMarkers(ordering); - return this; - } - - /** - * Adds a {@code GROUP BY} clause to this statement. - * - *

Note: support for {@code GROUP BY} clause is only available from Cassandra 3.10 onwards. - * - * @param columns the columns to group by. - * @return this statement. - * @throws IllegalStateException if a {@code GROUP BY} clause has already been provided. - */ - public Select groupBy(Object... columns) { - if (this.groupByColumnNames != null) - throw new IllegalStateException("A GROUP BY clause has already been provided"); - - this.groupByColumnNames = Arrays.asList(columns); - return this; - } - - /** - * Adds a {@code LIMIT} clause to this statement. - * - * @param limit the limit to set. - * @return this statement. - * @throws IllegalArgumentException if {@code limit <= 0}. - * @throws IllegalStateException if a {@code LIMIT} clause has already been provided. - */ - public Select limit(int limit) { - if (limit <= 0) - throw new IllegalArgumentException("Invalid LIMIT value, must be strictly positive"); - - if (this.limit != null) - throw new IllegalStateException("A LIMIT value has already been provided"); - - this.limit = limit; - setDirty(); - return this; - } - - /** - * Adds a prepared {@code LIMIT} clause to this statement. - * - * @param marker the marker to use for the limit. - * @return this statement. - * @throws IllegalStateException if a {@code LIMIT} clause has already been provided. - */ - public Select limit(BindMarker marker) { - if (this.limit != null) - throw new IllegalStateException("A LIMIT value has already been provided"); - - this.limit = marker; - checkForBindMarkers(marker); - return this; - } - - /** - * Adds a {@code PER PARTITION LIMIT} clause to this statement. - * - *

Note: support for {@code PER PARTITION LIMIT} clause is only available from Cassandra 3.6 - * onwards. - * - * @param perPartitionLimit the limit to set per partition. - * @return this statement. - * @throws IllegalArgumentException if {@code perPartitionLimit <= 0}. - * @throws IllegalStateException if a {@code PER PARTITION LIMIT} clause has already been - * provided. - * @throws IllegalStateException if this statement is a {@code SELECT DISTINCT} statement. - */ - public Select perPartitionLimit(int perPartitionLimit) { - if (perPartitionLimit <= 0) - throw new IllegalArgumentException( - "Invalid PER PARTITION LIMIT value, must be strictly positive"); - - if (this.perPartitionLimit != null) - throw new IllegalStateException("A PER PARTITION LIMIT value has already been provided"); - if (isDistinct) - throw new IllegalStateException( - "PER PARTITION LIMIT is not allowed with SELECT DISTINCT queries"); - - this.perPartitionLimit = perPartitionLimit; - setDirty(); - return this; - } - - /** - * Adds a prepared {@code PER PARTITION LIMIT} clause to this statement. - * - *

Note: support for {@code PER PARTITION LIMIT} clause is only available from Cassandra 3.6 - * onwards. - * - * @param marker the marker to use for the limit per partition. - * @return this statement. - * @throws IllegalStateException if a {@code PER PARTITION LIMIT} clause has already been - * provided. - * @throws IllegalStateException if this statement is a {@code SELECT DISTINCT} statement. - */ - public Select perPartitionLimit(BindMarker marker) { - if (this.perPartitionLimit != null) - throw new IllegalStateException("A PER PARTITION LIMIT value has already been provided"); - if (isDistinct) - throw new IllegalStateException( - "PER PARTITION LIMIT is not allowed with SELECT DISTINCT queries"); - - this.perPartitionLimit = marker; - checkForBindMarkers(marker); - return this; - } - - /** - * Adds an {@code ALLOW FILTERING} directive to this statement. - * - * @return this statement. - */ - public Select allowFiltering() { - allowFiltering = true; - return this; - } - - /** The {@code WHERE} clause of a {@code SELECT} statement. */ - public static class Where extends BuiltStatement.ForwardingStatement, BuildableQuery { + + /** + * Adds the provided GROUP BY clauses to the query. + * + *

As of version 4.0, Apache Cassandra only allows grouping by columns, therefore you can use + * the shortcuts {@link #groupByColumns(Iterable)} or {@link #groupByColumnIds(Iterable)}. + */ + @NonNull + Select groupBy(@NonNull Iterable selectors); + + /** Var-arg equivalent of {@link #groupBy(Iterable)}. */ + @NonNull + default Select groupBy(@NonNull Selector... selectors) { + return groupBy(Arrays.asList(selectors)); + } + + /** + * Shortcut for {@link #groupBy(Iterable)} where all the clauses are simple columns. The arguments + * are wrapped with {@link Selector#column(CqlIdentifier)}. + */ + @NonNull + default Select groupByColumnIds(@NonNull Iterable columnIds) { + return groupBy(Iterables.transform(columnIds, Selector::column)); + } + + /** Var-arg equivalent of {@link #groupByColumnIds(Iterable)}. */ + @NonNull + default Select groupByColumnIds(@NonNull CqlIdentifier... columnIds) { + return groupByColumnIds(Arrays.asList(columnIds)); + } + + /** + * Shortcut for {@link #groupBy(Iterable)} where all the clauses are simple columns. The arguments + * are wrapped with {@link Selector#column(String)}. + */ + @NonNull + default Select groupByColumns(@NonNull Iterable columnNames) { + return groupBy(Iterables.transform(columnNames, Selector::column)); + } + + /** Var-arg equivalent of {@link #groupByColumns(Iterable)}. */ + @NonNull + default Select groupByColumns(@NonNull String... columnNames) { + return groupByColumns(Arrays.asList(columnNames)); + } + + /** + * Adds the provided GROUP BY clause to the query. + * + *

As of version 4.0, Apache Cassandra only allows grouping by columns, therefore you can use + * the shortcuts {@link #groupBy(String)} or {@link #groupBy(CqlIdentifier)}. + */ + @NonNull + Select groupBy(@NonNull Selector selector); + + /** Shortcut for {@link #groupBy(Selector) groupBy(Selector.column(columnId))}. */ + @NonNull + default Select groupBy(@NonNull CqlIdentifier columnId) { + return groupBy(Selector.column(columnId)); + } + + /** Shortcut for {@link #groupBy(Selector) groupBy(Selector.column(columnName))}. */ + @NonNull + default Select groupBy(@NonNull String columnName) { + return groupBy(Selector.column(columnName)); + } + + /** + * Adds the provided ORDER BY clauses to the query. + * + *

They will be appended in the iteration order of the provided map. If an ordering was already + * defined for a given identifier, it will be removed and the new ordering will appear in its + * position in the provided map. + */ + @NonNull + Select orderByIds(@NonNull Map orderings); + + /** + * Shortcut for {@link #orderByIds(Map)} with the columns specified as case-insensitive names. + * They will be wrapped with {@link CqlIdentifier#fromCql(String)}. + * + *

Note that it's possible for two different case-insensitive names to resolve to the same + * identifier, for example "foo" and "Foo"; if this happens, a runtime exception will be thrown. + * + * @throws IllegalArgumentException if two names resolve to the same identifier. + */ + @NonNull + default Select orderBy(@NonNull Map orderings) { + return orderByIds(CqlIdentifiers.wrapKeys(orderings)); + } + + /** + * Adds the provided ORDER BY clause to the query. + * + *

If an ordering was already defined for this identifier, it will be removed and the new + * clause will be appended at the end of the current list for this query. + */ + @NonNull + Select orderBy(@NonNull CqlIdentifier columnId, @NonNull ClusteringOrder order); + + /** + * Shortcut for {@link #orderBy(CqlIdentifier, ClusteringOrder) + * orderBy(CqlIdentifier.fromCql(columnName), order)}. + */ + @NonNull + default Select orderBy(@NonNull String columnName, @NonNull ClusteringOrder order) { + return orderBy(CqlIdentifier.fromCql(columnName), order); + } + + /** + * Adds a LIMIT clause to this query with a literal value. + * + *

If this method or {@link #limit(BindMarker)} is called multiple times, the last value is + * used. + */ + @NonNull + Select limit(int limit); + + /** + * Adds a LIMIT clause to this query with a bind marker. + * + *

To create the argument, use one of the factory methods in {@link QueryBuilder}, for example + * {@link QueryBuilder#bindMarker() bindMarker()}. + * + *

If this method or {@link #limit(int)} is called multiple times, the last value is used. + * {@code null} can be passed to cancel a previous limit. + */ + @NonNull + Select limit(@Nullable BindMarker bindMarker); + + /** + * Adds a PER PARTITION LIMIT clause to this query with a literal value. + * + *

If this method or {@link #perPartitionLimit(BindMarker)} is called multiple times, the last + * value is used. + */ + @NonNull + Select perPartitionLimit(int limit); + + /** + * Adds a PER PARTITION LIMIT clause to this query with a bind marker. + * + *

To create the argument, use one of the factory methods in {@link QueryBuilder}, for example + * {@link QueryBuilder#bindMarker() bindMarker()}. + * + *

If this method or {@link #perPartitionLimit(int)} is called multiple times, the last value + * is used. {@code null} can be passed to cancel a previous limit. + */ + @NonNull + Select perPartitionLimit(@Nullable BindMarker bindMarker); + + /** + * Adds an ALLOW FILTERING clause to this query. + * + *

This method is idempotent, calling it multiple times will only add a single clause. + */ + @NonNull + Select allowFiltering(); +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/select/SelectFrom.java b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/select/SelectFrom.java new file mode 100644 index 00000000000..c4abb196a04 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/select/SelectFrom.java @@ -0,0 +1,36 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.select; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The beginning of a SELECT query. + * + *

It only knows about the table, and optionally whether the statement uses JSON or DISTINCT. It + * is not buildable yet: at least one selector needs to be specified. + */ +public interface SelectFrom extends OngoingSelection { + + // Implementation note - this interface exists to make the following a compile-time error: + // selectFrom("foo").distinct().build() + + @NonNull + SelectFrom json(); + + @NonNull + SelectFrom distinct(); +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/select/Selector.java b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/select/Selector.java new file mode 100644 index 00000000000..f3654f74e6f --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/select/Selector.java @@ -0,0 +1,562 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.querybuilder.CqlSnippet; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.ArithmeticOperator; +import com.datastax.oss.driver.internal.querybuilder.select.AllSelector; +import com.datastax.oss.driver.internal.querybuilder.select.BinaryArithmeticSelector; +import com.datastax.oss.driver.internal.querybuilder.select.CastSelector; +import com.datastax.oss.driver.internal.querybuilder.select.ColumnSelector; +import com.datastax.oss.driver.internal.querybuilder.select.CountAllSelector; +import com.datastax.oss.driver.internal.querybuilder.select.ElementSelector; +import com.datastax.oss.driver.internal.querybuilder.select.FieldSelector; +import com.datastax.oss.driver.internal.querybuilder.select.FunctionSelector; +import com.datastax.oss.driver.internal.querybuilder.select.ListSelector; +import com.datastax.oss.driver.internal.querybuilder.select.MapSelector; +import com.datastax.oss.driver.internal.querybuilder.select.OppositeSelector; +import com.datastax.oss.driver.internal.querybuilder.select.RangeSelector; +import com.datastax.oss.driver.internal.querybuilder.select.SetSelector; +import com.datastax.oss.driver.internal.querybuilder.select.TupleSelector; +import com.datastax.oss.driver.internal.querybuilder.select.TypeHintSelector; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Arrays; +import java.util.Map; + +/** + * A selected element in a SELECT query. + * + *

To build instances of this type, use the factory methods, such as {@link + * #column(CqlIdentifier) column}, {@link #function(CqlIdentifier, Iterable) function}, etc. + * + *

They are used as arguments to the {@link OngoingSelection#selectors(Iterable) selectors} + * method, for example: + * + *

{@code
+ * selectFrom("foo").selectors(Selector.column("bar"), Selector.column("baz"))
+ * // SELECT bar,baz FROM foo
+ * }
+ * + *

There are also shortcuts in the fluent API when you build a statement, for example: + * + *

{@code
+ * selectFrom("foo").column("bar").column("baz")
+ * // SELECT bar,baz FROM foo
+ * }
+ */ +public interface Selector extends CqlSnippet { + + /** Selects all columns, as in {@code SELECT *}. */ + @NonNull + static Selector all() { + return AllSelector.INSTANCE; + } + + /** Selects the count of all returned rows, as in {@code SELECT count(*)}. */ + @NonNull + static Selector countAll() { + return new CountAllSelector(); + } + + /** Selects a particular column by its CQL identifier. */ + @NonNull + static Selector column(@NonNull CqlIdentifier columnId) { + return new ColumnSelector(columnId); + } + + /** Shortcut for {@link #column(CqlIdentifier) column(CqlIdentifier.fromCql(columnName))} */ + @NonNull + static Selector column(@NonNull String columnName) { + return column(CqlIdentifier.fromCql(columnName)); + } + + /** + * Selects the sum of two arguments, as in {@code SELECT col1 + col2}. + * + *

This is available in Cassandra 4 and above. + */ + @NonNull + static Selector add(@NonNull Selector left, @NonNull Selector right) { + return new BinaryArithmeticSelector(ArithmeticOperator.SUM, left, right); + } + + /** + * Selects the difference of two arguments, as in {@code SELECT col1 - col2}. + * + *

This is available in Cassandra 4 and above. + */ + @NonNull + static Selector subtract(@NonNull Selector left, @NonNull Selector right) { + return new BinaryArithmeticSelector(ArithmeticOperator.DIFFERENCE, left, right); + } + + /** + * Selects the product of two arguments, as in {@code SELECT col1 * col2}. + * + *

This is available in Cassandra 4 and above. + * + *

The arguments will be parenthesized if they are instances of {@link #add} or {@link + * #subtract}. If they are raw selectors, you might have to parenthesize them yourself. + */ + @NonNull + static Selector multiply(@NonNull Selector left, @NonNull Selector right) { + return new BinaryArithmeticSelector(ArithmeticOperator.PRODUCT, left, right); + } + + /** + * Selects the quotient of two arguments, as in {@code SELECT col1 / col2}. + * + *

This is available in Cassandra 4 and above. + * + *

The arguments will be parenthesized if they are instances of {@link #add} or {@link + * #subtract}. If they are raw selectors, you might have to parenthesize them yourself. + */ + @NonNull + static Selector divide(@NonNull Selector left, @NonNull Selector right) { + return new BinaryArithmeticSelector(ArithmeticOperator.QUOTIENT, left, right); + } + + /** + * Selects the remainder of two arguments, as in {@code SELECT col1 % col2}. + * + *

This is available in Cassandra 4 and above. + * + *

The arguments will be parenthesized if they are instances of {@link #add} or {@link + * #subtract}. If they are raw selectors, you might have to parenthesize them yourself. + */ + @NonNull + static Selector remainder(@NonNull Selector left, @NonNull Selector right) { + return new BinaryArithmeticSelector(ArithmeticOperator.REMAINDER, left, right); + } + + /** + * Selects the opposite of an argument, as in {@code SELECT -col1}. + * + *

This is available in Cassandra 4 and above. + * + *

The argument will be parenthesized if it is an instance of {@link #add} or {@link + * #subtract}. If it is a raw selector, you might have to parenthesize it yourself. + */ + @NonNull + static Selector negate(@NonNull Selector argument) { + return new OppositeSelector(argument); + } + + /** Selects a field inside of a UDT column, as in {@code SELECT user.name}. */ + @NonNull + static Selector field(@NonNull Selector udt, @NonNull CqlIdentifier fieldId) { + return new FieldSelector(udt, fieldId); + } + + /** + * Shortcut for {@link #field(Selector, CqlIdentifier) getUdtField(udt, + * CqlIdentifier.fromCql(fieldName))}. + */ + @NonNull + static Selector field(@NonNull Selector udt, @NonNull String fieldName) { + return field(udt, CqlIdentifier.fromCql(fieldName)); + } + + /** + * Shortcut to select a UDT field when the UDT is a simple column (as opposed to a more complex + * selection, like a nested UDT). + */ + @NonNull + static Selector field(@NonNull CqlIdentifier udtColumnId, @NonNull CqlIdentifier fieldId) { + return field(column(udtColumnId), fieldId); + } + + /** + * Shortcut for {@link #field(CqlIdentifier, CqlIdentifier) + * field(CqlIdentifier.fromCql(udtColumnName), CqlIdentifier.fromCql(fieldName))}. + */ + @NonNull + static Selector field(@NonNull String udtColumnName, @NonNull String fieldName) { + return field(CqlIdentifier.fromCql(udtColumnName), CqlIdentifier.fromCql(fieldName)); + } + + /** + * Selects an element in a collection column, as in {@code SELECT m['key']}. + * + *

As of Cassandra 4, this is only allowed in SELECT for map and set columns. DELETE accepts + * list elements as well. + */ + @NonNull + static Selector element(@NonNull Selector collection, @NonNull Term index) { + return new ElementSelector(collection, index); + } + + /** + * Shortcut for element selection when the target collection is a simple column. + * + *

In other words, this is the equivalent of {@link #element(Selector, Term) + * element(column(collectionId), index)}. + */ + @NonNull + static Selector element(@NonNull CqlIdentifier collectionId, @NonNull Term index) { + return element(column(collectionId), index); + } + + /** + * Shortcut for {@link #element(CqlIdentifier, Term) + * element(CqlIdentifier.fromCql(collectionName), index)}. + */ + @NonNull + static Selector element(@NonNull String collectionName, @NonNull Term index) { + return element(CqlIdentifier.fromCql(collectionName), index); + } + + /** + * Selects a slice in a collection column, as in {@code SELECT s[4..8]}. + * + *

As of Cassandra 4, this is only allowed for set and map columns. Those collections are + * ordered, the elements (or keys in the case of a map), will be compared to the bounds for + * inclusions. Either bound can be unspecified, but not both. + * + * @param left the left bound (inclusive). Can be {@code null} to indicate that the slice is only + * right-bound. + * @param right the right bound (inclusive). Can be {@code null} to indicate that the slice is + * only left-bound. + */ + @NonNull + static Selector range(@NonNull Selector collection, @Nullable Term left, @Nullable Term right) { + return new RangeSelector(collection, left, right); + } + + /** + * Shortcut for slice selection when the target collection is a simple column. + * + *

In other words, this is the equivalent of {@link #range(Selector, Term, Term)} + * range(column(collectionId), left, right)}. + */ + @NonNull + static Selector range( + @NonNull CqlIdentifier collectionId, @Nullable Term left, @Nullable Term right) { + return range(column(collectionId), left, right); + } + + /** + * Shortcut for {@link #range(CqlIdentifier, Term, Term) + * range(CqlIdentifier.fromCql(collectionName), left, right)}. + */ + @NonNull + static Selector range(@NonNull String collectionName, @Nullable Term left, @Nullable Term right) { + return range(CqlIdentifier.fromCql(collectionName), left, right); + } + + /** + * Selects a group of elements as a list, as in {@code SELECT [a,b,c]}. + * + *

None of the selectors should be aliased (the query builder checks this at runtime), and they + * should all produce the same data type (the query builder can't check this, so the query will + * fail at execution time). + * + * @throws IllegalArgumentException if any of the selectors is aliased. + */ + @NonNull + static Selector listOf(@NonNull Iterable elementSelectors) { + return new ListSelector(elementSelectors); + } + + /** Var-arg equivalent of {@link #listOf(Iterable)}. */ + @NonNull + static Selector listOf(@NonNull Selector... elementSelectors) { + return listOf(Arrays.asList(elementSelectors)); + } + + /** + * Selects a group of elements as a set, as in {@code SELECT {a,b,c}}. + * + *

None of the selectors should be aliased (the query builder checks this at runtime), and they + * should all produce the same data type (the query builder can't check this, so the query will + * fail at execution time). + * + * @throws IllegalArgumentException if any of the selectors is aliased. + */ + @NonNull + static Selector setOf(@NonNull Iterable elementSelectors) { + return new SetSelector(elementSelectors); + } + + /** Var-arg equivalent of {@link #setOf(Iterable)}. */ + @NonNull + static Selector setOf(@NonNull Selector... elementSelectors) { + return setOf(Arrays.asList(elementSelectors)); + } + + /** + * Selects a group of elements as a tuple, as in {@code SELECT (a,b,c)}. + * + *

None of the selectors should be aliased (the query builder checks this at runtime). + * + * @throws IllegalArgumentException if any of the selectors is aliased. + */ + @NonNull + static Selector tupleOf(@NonNull Iterable elementSelectors) { + return new TupleSelector(elementSelectors); + } + + /** Var-arg equivalent of {@link #tupleOf(Iterable)}. */ + @NonNull + static Selector tupleOf(@NonNull Selector... elementSelectors) { + return tupleOf(Arrays.asList(elementSelectors)); + } + + /** + * Selects a group of elements as a map, as in {@code SELECT {a:b,c:d}}. + * + *

None of the selectors should be aliased (the query builder checks this at runtime). In + * addition, all key selectors should produce the same type, and all value selectors as well (the + * key and value types can be different); the query builder can't check this, so the query will + * fail at execution time if the types are not uniform. + * + *

Note that Cassandra often has trouble inferring the exact map type. This will manifest as + * the error message: + * + *

+   *   Cannot infer type for term xxx in selection clause (try using a cast to force a type)
+   * 
+ * + * If you run into this, consider providing the types explicitly with {@link #mapOf(Map, DataType, + * DataType)}. + * + * @throws IllegalArgumentException if any of the selectors is aliased. + */ + @NonNull + static Selector mapOf(@NonNull Map elementSelectors) { + return mapOf(elementSelectors, null, null); + } + + /** + * Selects a group of elements as a map and force the resulting map type, as in {@code SELECT + * (map){a:b,c:d}}. + * + *

To create the data types, use the constants and static methods in {@link DataTypes}, or + * {@link QueryBuilder#udt(CqlIdentifier)}. + * + * @see #mapOf(Map) + */ + @NonNull + static Selector mapOf( + @NonNull Map elementSelectors, + @Nullable DataType keyType, + @Nullable DataType valueType) { + return new MapSelector(elementSelectors, keyType, valueType); + } + + /** + * Provides a type hint for a selector, as in {@code SELECT (double)1/3}. + * + *

To create the data type, use the constants and static methods in {@link DataTypes}, or + * {@link QueryBuilder#udt(CqlIdentifier)}. + */ + @NonNull + static Selector typeHint(@NonNull Selector selector, @NonNull DataType targetType) { + return new TypeHintSelector(selector, targetType); + } + + /** + * Selects the result of a function call, as is {@code SELECT f(a,b)} + * + *

None of the arguments should be aliased (the query builder checks this at runtime). + * + * @throws IllegalArgumentException if any of the selectors is aliased. + */ + @NonNull + static Selector function( + @NonNull CqlIdentifier functionId, @NonNull Iterable arguments) { + return new FunctionSelector(null, functionId, arguments); + } + + /** Var-arg equivalent of {@link #function(CqlIdentifier, Iterable)}. */ + @NonNull + static Selector function(@NonNull CqlIdentifier functionId, @NonNull Selector... arguments) { + return function(functionId, Arrays.asList(arguments)); + } + + /** + * Shortcut for {@link #function(CqlIdentifier, Iterable) + * function(CqlIdentifier.fromCql(functionName), arguments)}. + */ + @NonNull + static Selector function(@NonNull String functionName, @NonNull Iterable arguments) { + return function(CqlIdentifier.fromCql(functionName), arguments); + } + + /** Var-arg equivalent of {@link #function(String, Iterable)}. */ + @NonNull + static Selector function(@NonNull String functionName, @NonNull Selector... arguments) { + return function(functionName, Arrays.asList(arguments)); + } + + /** + * Selects the result of a function call, as is {@code SELECT ks.f(a,b)} + * + *

None of the arguments should be aliased (the query builder checks this at runtime). + * + * @throws IllegalArgumentException if any of the selectors is aliased. + */ + @NonNull + static Selector function( + @Nullable CqlIdentifier keyspaceId, + @NonNull CqlIdentifier functionId, + @NonNull Iterable arguments) { + return new FunctionSelector(keyspaceId, functionId, arguments); + } + + /** Var-arg equivalent of {@link #function(CqlIdentifier, CqlIdentifier, Iterable)}. */ + @NonNull + static Selector function( + @Nullable CqlIdentifier keyspaceId, + @NonNull CqlIdentifier functionId, + @NonNull Selector... arguments) { + return function(keyspaceId, functionId, Arrays.asList(arguments)); + } + + /** + * Shortcut for {@link #function(CqlIdentifier, CqlIdentifier, Iterable)} + * function(CqlIdentifier.fromCql(functionName), arguments)}. + */ + @NonNull + static Selector function( + @Nullable String keyspaceName, + @NonNull String functionName, + @NonNull Iterable arguments) { + return function( + keyspaceName == null ? null : CqlIdentifier.fromCql(keyspaceName), + CqlIdentifier.fromCql(functionName), + arguments); + } + + /** Var-arg equivalent of {@link #function(String, String, Iterable)}. */ + @NonNull + static Selector function( + @Nullable String keyspaceName, @NonNull String functionName, @NonNull Selector... arguments) { + return function(keyspaceName, functionName, Arrays.asList(arguments)); + } + + /** + * Shortcut to select the result of the built-in {@code writetime} function, as in {@code SELECT + * writetime(c)}. + */ + @NonNull + static Selector writeTime(@NonNull CqlIdentifier columnId) { + return function("writetime", column(columnId)); + } + + /** + * Shortcut for {@link #writeTime(CqlIdentifier) writeTime(CqlIdentifier.fromCql(columnName))}. + */ + @NonNull + static Selector writeTime(@NonNull String columnName) { + return writeTime(CqlIdentifier.fromCql(columnName)); + } + + /** + * Shortcut to select the result of the built-in {@code ttl} function, as in {@code SELECT + * ttl(c)}. + */ + @NonNull + static Selector ttl(@NonNull CqlIdentifier columnId) { + return function("ttl", column(columnId)); + } + + /** Shortcut for {@link #ttl(CqlIdentifier) ttl(CqlIdentifier.fromCql(columnName))}. */ + @NonNull + static Selector ttl(@NonNull String columnName) { + return ttl(CqlIdentifier.fromCql(columnName)); + } + + /** + * Casts a selector to a type, as in {@code SELECT CAST(a AS double)}. + * + *

To create the data type, use the constants and static methods in {@link DataTypes}, or + * {@link QueryBuilder#udt(CqlIdentifier)}. + * + * @throws IllegalArgumentException if the selector is aliased. + */ + @NonNull + static Selector cast(@NonNull Selector selector, @NonNull DataType targetType) { + return new CastSelector(selector, targetType); + } + + /** Shortcut to select the result of the built-in {@code toDate} function on a simple column. */ + @NonNull + static Selector toDate(@NonNull CqlIdentifier columnId) { + return function("todate", Selector.column(columnId)); + } + + /** Shortcut for {@link #toDate(CqlIdentifier) toDate(CqlIdentifier.fromCql(columnName))}. */ + @NonNull + static Selector toDate(@NonNull String columnName) { + return toDate(CqlIdentifier.fromCql(columnName)); + } + + /** + * Shortcut to select the result of the built-in {@code toTimestamp} function on a simple column. + */ + @NonNull + static Selector toTimestamp(@NonNull CqlIdentifier columnId) { + return function("totimestamp", Selector.column(columnId)); + } + + /** + * Shortcut for {@link #toTimestamp(CqlIdentifier) + * toTimestamp(CqlIdentifier.fromCql(columnName))}. + */ + @NonNull + static Selector toTimestamp(@NonNull String columnName) { + return toTimestamp(CqlIdentifier.fromCql(columnName)); + } + + /** + * Shortcut to select the result of the built-in {@code toUnixTimestamp} function on a simple + * column. + */ + @NonNull + static Selector toUnixTimestamp(@NonNull CqlIdentifier columnId) { + return function("tounixtimestamp", Selector.column(columnId)); + } + + /** + * Shortcut for {@link #toUnixTimestamp(CqlIdentifier) + * toUnixTimestamp(CqlIdentifier.fromCql(columnName))}. + */ + @NonNull + static Selector toUnixTimestamp(@NonNull String columnName) { + return toUnixTimestamp(CqlIdentifier.fromCql(columnName)); + } + + /** Aliases the selector, as in {@code SELECT count(*) AS total}. */ + @NonNull + Selector as(@NonNull CqlIdentifier alias); + + /** Shortcut for {@link #as(CqlIdentifier) as(CqlIdentifier.fromCql(alias))} */ + @NonNull + default Selector as(@NonNull String alias) { + return as(CqlIdentifier.fromCql(alias)); + } + + /** @return null if the selector is not aliased. */ + @Nullable + CqlIdentifier getAlias(); +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/term/Term.java b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/term/Term.java new file mode 100644 index 00000000000..2cf33b856ca --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/term/Term.java @@ -0,0 +1,68 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.term; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.BuildableQuery; +import com.datastax.oss.driver.api.querybuilder.CqlSnippet; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.select.OngoingSelection; +import com.datastax.oss.driver.api.querybuilder.select.Selector; + +/** + * A simple expression that doesn't reference columns. + * + *

It is used as an argument to certain {@linkplain Selector selectors} (for example the indices + * in a {@linkplain OngoingSelection#range(Selector, Term, Term) range}), or as the right operand of + * {@linkplain Relation relations}. + * + *

To create a term, call one of the static factory methods in {@link QueryBuilder}: + * + *

    + *
  • {@link QueryBuilder#literal(Object) literal()} to inline a Java object into the query + * string; + *
  • {@link QueryBuilder#function(CqlIdentifier, CqlIdentifier, Iterable) function()} to invoke + * a built-in or user-defined function; + *
  • an arithmetic operator combining other terms: {@link QueryBuilder#add(Term, Term) add()}, + * {@link QueryBuilder#subtract(Term, Term) subtract()}, {@link QueryBuilder#negate(Term) + * negate()}, {@link QueryBuilder#multiply(Term, Term) multiply()}, {@link + * QueryBuilder#divide(Term, Term) divide()} or {@link QueryBuilder#remainder(Term, Term) + * remainder()}; + *
  • {@link QueryBuilder#typeHint(Term, DataType) typeHint()} to coerce another term to a + * particular CQL type; + *
  • {@link QueryBuilder#raw(String) raw()} for a raw CQL snippet. + *
+ * + * Note that some of these methods have multiple overloads. + */ +public interface Term extends CqlSnippet { + + /** + * Whether the term is idempotent. + * + *

That is, whether it always produces the same result when used multiple times. For example, + * the literal {@code 1} is idempotent, the function call {@code now()} isn't. + * + *

This is used internally by the query builder to compute the {@link Statement#isIdempotent()} + * flag on the statements generated by {@link BuildableQuery#build()}. If a term is ambiguous (for + * example a raw snippet or a call to a user function), the builder is pessimistic and assumes the + * term is not idempotent. + */ + boolean isIdempotent(); +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/Assignment.java b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/Assignment.java new file mode 100644 index 00000000000..d918c8aba42 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/Assignment.java @@ -0,0 +1,399 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.querybuilder.BuildableQuery; +import com.datastax.oss.driver.api.querybuilder.CqlSnippet; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.lhs.ColumnComponentLeftOperand; +import com.datastax.oss.driver.internal.querybuilder.lhs.ColumnLeftOperand; +import com.datastax.oss.driver.internal.querybuilder.lhs.FieldLeftOperand; +import com.datastax.oss.driver.internal.querybuilder.update.AppendAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.AppendListElementAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.AppendMapEntryAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.AppendSetElementAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.CounterAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.DefaultAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.PrependAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.PrependListElementAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.PrependMapEntryAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.PrependSetElementAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.RemoveListElementAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.RemoveMapEntryAssignment; +import com.datastax.oss.driver.internal.querybuilder.update.RemoveSetElementAssignment; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** An assignment that appears after the SET keyword in an UPDATE statement. */ +public interface Assignment extends CqlSnippet { + + /** Assigns a value to a column, as in {@code SET c=?}. */ + @NonNull + static Assignment setColumn(@NonNull CqlIdentifier columnId, @NonNull Term value) { + return new DefaultAssignment(new ColumnLeftOperand(columnId), "=", value); + } + + /** + * Shortcut for {@link #setColumn(CqlIdentifier, Term) + * setColumn(CqlIdentifier.fromCql(columnName), value)}. + */ + @NonNull + static Assignment setColumn(@NonNull String columnName, @NonNull Term value) { + return setColumn(CqlIdentifier.fromCql(columnName), value); + } + + /** Assigns a value to a field of a UDT, as in {@code SET address.zip=?}. */ + @NonNull + static Assignment setField( + @NonNull CqlIdentifier columnId, @NonNull CqlIdentifier fieldId, @NonNull Term value) { + return new DefaultAssignment(new FieldLeftOperand(columnId, fieldId), "=", value); + } + + /** + * Shortcut for {@link #setField(CqlIdentifier, CqlIdentifier, Term) + * setField(CqlIdentifier.fromCql(columnName), CqlIdentifier.fromCql(fieldName), value)}. + */ + @NonNull + static Assignment setField( + @NonNull String columnName, @NonNull String fieldName, @NonNull Term value) { + return setField(CqlIdentifier.fromCql(columnName), CqlIdentifier.fromCql(fieldName), value); + } + + /** Assigns a value to an entry in a map column, as in {@code SET map[?]=?}. */ + @NonNull + static Assignment setMapValue( + @NonNull CqlIdentifier columnId, @NonNull Term index, @NonNull Term value) { + return new DefaultAssignment(new ColumnComponentLeftOperand(columnId, index), "=", value); + } + + /** + * Shortcut for {@link #setMapValue(CqlIdentifier, Term, Term) + * setMapValue(CqlIdentifier.fromCql(columnName), index, value)}. + */ + @NonNull + static Assignment setMapValue( + @NonNull String columnName, @NonNull Term index, @NonNull Term value) { + return setMapValue(CqlIdentifier.fromCql(columnName), index, value); + } + + /** Increments a counter, as in {@code SET c+=?}. */ + @NonNull + static Assignment increment(@NonNull CqlIdentifier columnId, @NonNull Term amount) { + return new CounterAssignment(new ColumnLeftOperand(columnId), "+=", amount); + } + + /** + * Shortcut for {@link #increment(CqlIdentifier, Term) + * increment(CqlIdentifier.fromCql(columnName), amount)} + */ + @NonNull + static Assignment increment(@NonNull String columnName, @NonNull Term amount) { + return increment(CqlIdentifier.fromCql(columnName), amount); + } + + /** Increments a counter by 1, as in {@code SET c+=1} . */ + @NonNull + static Assignment increment(@NonNull CqlIdentifier columnId) { + return increment(columnId, QueryBuilder.literal(1)); + } + + /** Shortcut for {@link #increment(CqlIdentifier) CqlIdentifier.fromCql(columnName)}. */ + @NonNull + static Assignment increment(@NonNull String columnName) { + return increment(CqlIdentifier.fromCql(columnName)); + } + + /** Decrements a counter, as in {@code SET c-=?}. */ + @NonNull + static Assignment decrement(@NonNull CqlIdentifier columnId, @NonNull Term amount) { + return new CounterAssignment(new ColumnLeftOperand(columnId), "-=", amount); + } + + /** + * Shortcut for {@link #decrement(CqlIdentifier, Term) + * decrement(CqlIdentifier.fromCql(columnName), amount)} + */ + @NonNull + static Assignment decrement(@NonNull String columnName, @NonNull Term amount) { + return decrement(CqlIdentifier.fromCql(columnName), amount); + } + + /** Decrements a counter by 1, as in {@code SET c-=1} . */ + @NonNull + static Assignment decrement(@NonNull CqlIdentifier columnId) { + return decrement(columnId, QueryBuilder.literal(1)); + } + + /** Shortcut for {@link #decrement(CqlIdentifier) CqlIdentifier.fromCql(columnName)}. */ + @NonNull + static Assignment decrement(@NonNull String columnName) { + return decrement(CqlIdentifier.fromCql(columnName)); + } + + /** + * Appends to a collection column, as in {@code SET l+=?}. + * + *

The term must be a collection of the same type as the column. + */ + @NonNull + static Assignment append(@NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return new AppendAssignment(new ColumnLeftOperand(columnId), suffix); + } + + /** + * Shortcut for {@link #append(CqlIdentifier, Term) append(CqlIdentifier.fromCql(columnName), + * suffix)}. + */ + @NonNull + static Assignment append(@NonNull String columnName, @NonNull Term suffix) { + return append(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Appends a single element to a list column, as in {@code SET l+=[?]}. + * + *

The term must be of the same type as the column's elements. + */ + @NonNull + static Assignment appendListElement(@NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return new AppendListElementAssignment(columnId, suffix); + } + + /** + * Shortcut for {@link #appendListElement(CqlIdentifier, Term) + * appendListElement(CqlIdentifier.fromCql(columnName), suffix)}. + */ + @NonNull + static Assignment appendListElement(@NonNull String columnName, @NonNull Term suffix) { + return appendListElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Appends a single element to a set column, as in {@code SET s+={?}}. + * + *

The term must be of the same type as the column's elements. + */ + @NonNull + static Assignment appendSetElement(@NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return new AppendSetElementAssignment(columnId, suffix); + } + + /** + * Shortcut for {@link #appendSetElement(CqlIdentifier, Term) + * appendSetElement(CqlIdentifier.fromCql(columnName), suffix)}. + */ + @NonNull + static Assignment appendSetElement(@NonNull String columnName, @NonNull Term suffix) { + return appendSetElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Appends a single entry to a map column, as in {@code SET m+={?:?}}. + * + *

The terms must be of the same type as the column's keys and values respectively. + */ + @NonNull + static Assignment appendMapEntry( + @NonNull CqlIdentifier columnId, @NonNull Term key, @NonNull Term value) { + return new AppendMapEntryAssignment(columnId, key, value); + } + + /** + * Shortcut for {@link #appendMapEntry(CqlIdentifier, Term, Term) + * appendMapEntry(CqlIdentifier.fromCql(columnName), key, value)}. + */ + @NonNull + static Assignment appendMapEntry( + @NonNull String columnName, @NonNull Term key, @NonNull Term value) { + return appendMapEntry(CqlIdentifier.fromCql(columnName), key, value); + } + + /** + * Prepends to a collection column, as in {@code SET l=[1,2,3]+l}. + * + *

The term must be a collection of the same type as the column. + */ + @NonNull + static Assignment prepend(@NonNull CqlIdentifier columnId, @NonNull Term prefix) { + return new PrependAssignment(columnId, prefix); + } + + /** + * Shortcut for {@link #prepend(CqlIdentifier, Term) prepend(CqlIdentifier.fromCql(columnName), + * prefix)}. + */ + @NonNull + static Assignment prepend(@NonNull String columnName, @NonNull Term prefix) { + return prepend(CqlIdentifier.fromCql(columnName), prefix); + } + + /** + * Prepends a single element to a list column, as in {@code SET l=[?]+l}. + * + *

The term must be of the same type as the column's elements. + */ + @NonNull + static Assignment prependListElement(@NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return new PrependListElementAssignment(columnId, suffix); + } + + /** + * Shortcut for {@link #prependListElement(CqlIdentifier, Term) + * prependListElement(CqlIdentifier.fromCql(columnName), suffix)}. + */ + @NonNull + static Assignment prependListElement(@NonNull String columnName, @NonNull Term suffix) { + return prependListElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Prepends a single element to a set column, as in {@code SET s={?}+s}. + * + *

The term must be of the same type as the column's elements. + */ + @NonNull + static Assignment prependSetElement(@NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return new PrependSetElementAssignment(columnId, suffix); + } + + /** + * Shortcut for {@link #prependSetElement(CqlIdentifier, Term) + * prependSetElement(CqlIdentifier.fromCql(columnName), suffix)}. + */ + @NonNull + static Assignment prependSetElement(@NonNull String columnName, @NonNull Term suffix) { + return prependSetElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Prepends a single entry to a map column, as in {@code SET m={?:?}+m}. + * + *

The terms must be of the same type as the column's keys and values respectively. + */ + @NonNull + static Assignment prependMapEntry( + @NonNull CqlIdentifier columnId, @NonNull Term key, @NonNull Term value) { + return new PrependMapEntryAssignment(columnId, key, value); + } + + /** + * Shortcut for {@link #prependMapEntry(CqlIdentifier, Term, Term) + * prependMapEntry(CqlIdentifier.fromCql(columnName), key, value)}. + */ + @NonNull + static Assignment prependMapEntry( + @NonNull String columnName, @NonNull Term key, @NonNull Term value) { + return prependMapEntry(CqlIdentifier.fromCql(columnName), key, value); + } + + /** + * Removes elements from a collection, as in {@code SET l-=[1,2,3]}. + * + *

The term must be a collection of the same type as the column. + * + *

DO NOT USE THIS TO DECREMENT COUNTERS. Use the dedicated {@link + * #decrement(CqlIdentifier, Term)} methods instead. While the operator is technically the same, + * and it would be possible to generate an expression such as {@code counter-=1} with this method, + * a collection removal is idempotent while a counter decrement isn't. + */ + @NonNull + static Assignment remove(@NonNull CqlIdentifier columnId, @NonNull Term collectionToRemove) { + return new DefaultAssignment(new ColumnLeftOperand(columnId), "-=", collectionToRemove); + } + + /** + * Shortcut for {@link #remove(CqlIdentifier, Term) remove(CqlIdentifier.fromCql(columnName), + * collectionToRemove)}. + */ + @NonNull + static Assignment remove(@NonNull String columnName, @NonNull Term collectionToRemove) { + return remove(CqlIdentifier.fromCql(columnName), collectionToRemove); + } + + /** + * Removes a single element to a list column, as in {@code SET l-=[?]}. + * + *

The term must be of the same type as the column's elements. + */ + @NonNull + static Assignment removeListElement(@NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return new RemoveListElementAssignment(columnId, suffix); + } + + /** + * Shortcut for {@link #removeListElement(CqlIdentifier, Term) + * removeListElement(CqlIdentifier.fromCql(columnName), suffix)}. + */ + @NonNull + static Assignment removeListElement(@NonNull String columnName, @NonNull Term suffix) { + return removeListElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Removes a single element to a set column, as in {@code SET s-={?}}. + * + *

The term must be of the same type as the column's elements. + */ + @NonNull + static Assignment removeSetElement(@NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return new RemoveSetElementAssignment(columnId, suffix); + } + + /** + * Shortcut for {@link #removeSetElement(CqlIdentifier, Term) + * removeSetElement(CqlIdentifier.fromCql(columnName), suffix)}. + */ + @NonNull + static Assignment removeSetElement(@NonNull String columnName, @NonNull Term suffix) { + return removeSetElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Removes a single entry to a map column, as in {@code SET m-={?:?}}. + * + *

The terms must be of the same type as the column's keys and values respectively. + */ + @NonNull + static Assignment removeMapEntry( + @NonNull CqlIdentifier columnId, @NonNull Term key, @NonNull Term value) { + return new RemoveMapEntryAssignment(columnId, key, value); + } + + /** + * Shortcut for {@link #removeMapEntry(CqlIdentifier, Term, Term) + * removeMapEntry(CqlIdentifier.fromCql(columnName), key, value)}. + */ + @NonNull + static Assignment removeMapEntry( + @NonNull String columnName, @NonNull Term key, @NonNull Term value) { + return removeMapEntry(CqlIdentifier.fromCql(columnName), key, value); + } + + /** + * Whether this assignment is idempotent. + * + *

That is, whether it always sets its target column to the same value when used multiple + * times. For example, {@code UPDATE ... SET c=1} is idempotent, {@code SET l=l+[1]} isn't. + * + *

This is used internally by the query builder to compute the {@link Statement#isIdempotent()} + * flag on the UPDATE statements generated by {@link BuildableQuery#build()}. If an assignment is + * ambiguous (for example a raw snippet or a call to a user function in the right operands), the + * builder is pessimistic and assumes the term is not idempotent. + */ + boolean isIdempotent(); +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/OngoingAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/OngoingAssignment.java new file mode 100644 index 00000000000..67af1f09e34 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/OngoingAssignment.java @@ -0,0 +1,551 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Arrays; + +public interface OngoingAssignment { + + /** + * Adds an assignment to this statement, as in {@code UPDATE foo SET v=1}. + * + *

To create the argument, use one of the factory methods in {@link Assignment}, for example + * Assignment{@link #setColumn(CqlIdentifier, Term)}. This type also provides shortcuts to create + * and add the assignment in one call, for example {@link #setColumn(CqlIdentifier, Term)}. + * + *

If you add multiple assignments as one, consider {@link #set(Iterable)} as a more efficient + * alternative. + */ + @NonNull + UpdateWithAssignments set(@NonNull Assignment assignment); + + /** + * Adds multiple assignments at once. + * + *

This is slightly more efficient than adding the assignments one by one (since the underlying + * implementation of this object is immutable). + * + *

To create the argument, use one of the factory methods in {@link Assignment}, for example + * Assignment{@link #setColumn(CqlIdentifier, Term)}. + */ + @NonNull + UpdateWithAssignments set(@NonNull Iterable additionalAssignments); + + /** Var-arg equivalent of {@link #set(Iterable)}. */ + @NonNull + default UpdateWithAssignments set(@NonNull Assignment... assignments) { + return set(Arrays.asList(assignments)); + } + + /** + * Assigns a value to a column, as in {@code SET c=1}. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.setColumn(columnId, value))}. + * + * @see Assignment#setColumn(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments setColumn(@NonNull CqlIdentifier columnId, @NonNull Term value) { + return set(Assignment.setColumn(columnId, value)); + } + + /** + * Shortcut for {@link #setColumn(CqlIdentifier, Term) + * setColumn(CqlIdentifier.fromCql(columnName), value)}. + * + * @see Assignment#setColumn(String, Term) + */ + @NonNull + default UpdateWithAssignments setColumn(@NonNull String columnName, @NonNull Term value) { + return setColumn(CqlIdentifier.fromCql(columnName), value); + } + + /** + * Assigns a value to a field of a UDT, as in {@code SET address.zip=?}. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.setField(columnId, fieldId, + * value))}. + * + * @see Assignment#setField(CqlIdentifier, CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments setField( + @NonNull CqlIdentifier columnId, @NonNull CqlIdentifier fieldId, @NonNull Term value) { + return set(Assignment.setField(columnId, fieldId, value)); + } + + /** + * Shortcut for {@link #setField(CqlIdentifier, CqlIdentifier, Term) + * setField(CqlIdentifier.fromCql(columnName), CqlIdentifier.fromCql(fieldName), value)}. + * + * @see Assignment#setField(String, String, Term) + */ + @NonNull + default UpdateWithAssignments setField( + @NonNull String columnName, @NonNull String fieldName, @NonNull Term value) { + return setField(CqlIdentifier.fromCql(columnName), CqlIdentifier.fromCql(fieldName), value); + } + + /** + * Assigns a value to an entry in a map column, as in {@code SET map[?]=?}. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.setMapValue(columnId, index, + * value))}. + * + * @see Assignment#setMapValue(CqlIdentifier, Term, Term) + */ + @NonNull + default UpdateWithAssignments setMapValue( + @NonNull CqlIdentifier columnId, @NonNull Term index, @NonNull Term value) { + return set(Assignment.setMapValue(columnId, index, value)); + } + + /** + * Shortcut for {@link #setMapValue(CqlIdentifier, Term, Term) + * setMapValue(CqlIdentifier.fromCql(columnName), index, value)}. + * + * @see Assignment#setMapValue(String, Term, Term) + */ + @NonNull + default UpdateWithAssignments setMapValue( + @NonNull String columnName, @NonNull Term index, @NonNull Term value) { + return setMapValue(CqlIdentifier.fromCql(columnName), index, value); + } + + /** + * Increments a counter, as in {@code SET c+=?}. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.increment(columnId, amount))}. + * + * @see Assignment#increment(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments increment(@NonNull CqlIdentifier columnId, @NonNull Term amount) { + return set(Assignment.increment(columnId, amount)); + } + + /** + * Shortcut for {@link #increment(CqlIdentifier, Term) + * increment(CqlIdentifier.fromCql(columnName), amount)} + * + * @see Assignment#increment(String, Term) + */ + @NonNull + default UpdateWithAssignments increment(@NonNull String columnName, @NonNull Term amount) { + return increment(CqlIdentifier.fromCql(columnName), amount); + } + + /** + * Increments a counter by 1, as in {@code SET c+=1} . + * + *

This is a shortcut for {@link #increment(CqlIdentifier, Term)} increment(columnId, + * QueryBuilder.literal(1))}. + * + * @see Assignment#increment(CqlIdentifier) + */ + @NonNull + default UpdateWithAssignments increment(@NonNull CqlIdentifier columnId) { + return increment(columnId, QueryBuilder.literal(1)); + } + + /** + * Shortcut for {@link #increment(CqlIdentifier) CqlIdentifier.fromCql(columnName)}. + * + * @see Assignment#increment(CqlIdentifier) + */ + @NonNull + default UpdateWithAssignments increment(@NonNull String columnName) { + return increment(CqlIdentifier.fromCql(columnName)); + } + + /** + * Decrements a counter, as in {@code SET c-=?}. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.decrement(columnId, amount))}. + * + * @see Assignment#decrement(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments decrement(@NonNull CqlIdentifier columnId, @NonNull Term amount) { + return set(Assignment.decrement(columnId, amount)); + } + + /** + * Shortcut for {@link #decrement(CqlIdentifier, Term) + * decrement(CqlIdentifier.fromCql(columnName), amount)} + * + * @see Assignment#decrement(String, Term) + */ + @NonNull + default UpdateWithAssignments decrement(@NonNull String columnName, @NonNull Term amount) { + return decrement(CqlIdentifier.fromCql(columnName), amount); + } + + /** + * Decrements a counter by 1, as in {@code SET c-=1}. + * + *

This is a shortcut for {@link #decrement(CqlIdentifier, Term)} decrement(columnId, 1)}. + * + * @see Assignment#decrement(CqlIdentifier) + */ + @NonNull + default UpdateWithAssignments decrement(@NonNull CqlIdentifier columnId) { + return decrement(columnId, QueryBuilder.literal(1)); + } + + /** + * Shortcut for {@link #decrement(CqlIdentifier) CqlIdentifier.fromCql(columnName)}. + * + * @see Assignment#decrement(String) + */ + @NonNull + default UpdateWithAssignments decrement(@NonNull String columnName) { + return decrement(CqlIdentifier.fromCql(columnName)); + } + + /** + * Appends to a collection column, as in {@code SET l+=?}. + * + *

The term must be a collection of the same type as the column. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.append(columnId, suffix))}. + * + * @see Assignment#append(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments append(@NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return set(Assignment.append(columnId, suffix)); + } + + /** + * Shortcut for {@link #append(CqlIdentifier, Term) append(CqlIdentifier.fromCql(columnName), + * suffix)}. + * + * @see Assignment#append(String, Term) + */ + @NonNull + default UpdateWithAssignments append(@NonNull String columnName, @NonNull Term suffix) { + return append(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Appends a single element to a list column, as in {@code SET l+=[?]}. + * + *

The term must be of the same type as the column's elements. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.appendListElement(columnId, + * suffix))}. + * + * @see Assignment#appendListElement(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments appendListElement( + @NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return set(Assignment.appendListElement(columnId, suffix)); + } + + /** + * Shortcut for {@link #appendListElement(CqlIdentifier, Term) + * appendListElement(CqlIdentifier.fromCql(columnName), suffix)}. + * + * @see Assignment#appendListElement(String, Term) + */ + @NonNull + default UpdateWithAssignments appendListElement( + @NonNull String columnName, @NonNull Term suffix) { + return appendListElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Appends a single element to a set column, as in {@code SET s+={?}}. + * + *

The term must be of the same type as the column's elements. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.appendSetElement(columnId, + * suffix))}. + * + * @see Assignment#appendSetElement(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments appendSetElement( + @NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return set(Assignment.appendSetElement(columnId, suffix)); + } + + /** + * Shortcut for {@link #appendSetElement(CqlIdentifier, Term) + * appendSetElement(CqlIdentifier.fromCql(columnName), suffix)}. + */ + @NonNull + default UpdateWithAssignments appendSetElement(@NonNull String columnName, @NonNull Term suffix) { + return appendSetElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Appends a single entry to a map column, as in {@code SET m+={?:?}}. + * + *

The terms must be of the same type as the column's keys and values respectively. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.appendMapEntry(columnId, key, + * value)}. + * + * @see Assignment#appendMapEntry(CqlIdentifier, Term, Term) + */ + @NonNull + default UpdateWithAssignments appendMapEntry( + @NonNull CqlIdentifier columnId, @NonNull Term key, @NonNull Term value) { + return set(Assignment.appendMapEntry(columnId, key, value)); + } + + /** + * Shortcut for {@link #appendMapEntry(CqlIdentifier, Term, Term) + * appendMapEntry(CqlIdentifier.fromCql(columnName), key, value)}. + * + * @see Assignment#appendMapEntry(String, Term, Term) + */ + @NonNull + default UpdateWithAssignments appendMapEntry( + @NonNull String columnName, @NonNull Term key, @NonNull Term value) { + return appendMapEntry(CqlIdentifier.fromCql(columnName), key, value); + } + + /** + * Prepends to a collection column, as in {@code SET l=[1,2,3]+l}. + * + *

The term must be a collection of the same type as the column. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.prepend(columnId, prefix))}. + * + * @see Assignment#prepend(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments prepend(@NonNull CqlIdentifier columnId, @NonNull Term prefix) { + return set(Assignment.prepend(columnId, prefix)); + } + + /** + * Shortcut for {@link #prepend(CqlIdentifier, Term) prepend(CqlIdentifier.fromCql(columnName), + * prefix)}. + * + * @see Assignment#prepend(String, Term) + */ + @NonNull + default UpdateWithAssignments prepend(@NonNull String columnName, @NonNull Term prefix) { + return prepend(CqlIdentifier.fromCql(columnName), prefix); + } + + /** + * Prepends a single element to a list column, as in {@code SET l=[?]+l}. + * + *

The term must be of the same type as the column's elements. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.prependListElement(columnId, + * suffix))}. + * + * @see Assignment#prependListElement(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments prependListElement( + @NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return set(Assignment.prependListElement(columnId, suffix)); + } + + /** + * Shortcut for {@link #prependListElement(CqlIdentifier, Term) + * prependListElement(CqlIdentifier.fromCql(columnName), suffix)}. + * + * @see Assignment#prependListElement(String, Term) + */ + @NonNull + default UpdateWithAssignments prependListElement( + @NonNull String columnName, @NonNull Term suffix) { + return prependListElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Prepends a single element to a set column, as in {@code SET s={?}+s}. + * + *

The term must be of the same type as the column's elements. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.prependSetElement(columnId, + * suffix))}. + * + * @see Assignment#prependSetElement(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments prependSetElement( + @NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return set(Assignment.prependSetElement(columnId, suffix)); + } + + /** + * Shortcut for {@link #prependSetElement(CqlIdentifier, Term) + * prependSetElement(CqlIdentifier.fromCql(columnName), suffix)}. + * + * @see Assignment#prependSetElement(String, Term) + */ + @NonNull + default UpdateWithAssignments prependSetElement( + @NonNull String columnName, @NonNull Term suffix) { + return prependSetElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Prepends a single entry to a map column, as in {@code SET m={?:?}+m}. + * + *

The terms must be of the same type as the column's keys and values respectively. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.prependMapEntry(columnId, key, + * value))}. + * + * @see Assignment#prependMapEntry(CqlIdentifier, Term, Term) + */ + @NonNull + default UpdateWithAssignments prependMapEntry( + @NonNull CqlIdentifier columnId, @NonNull Term key, @NonNull Term value) { + return set(Assignment.prependMapEntry(columnId, key, value)); + } + + /** + * Shortcut for {@link #prependMapEntry(CqlIdentifier, Term, Term) + * prependMapEntry(CqlIdentifier.fromCql(columnName), key, value)}. + * + * @see Assignment#prependMapEntry(String, Term, Term) + */ + @NonNull + default UpdateWithAssignments prependMapEntry( + @NonNull String columnName, @NonNull Term key, @NonNull Term value) { + return prependMapEntry(CqlIdentifier.fromCql(columnName), key, value); + } + + /** + * Removes elements from a collection, as in {@code SET l-=[1,2,3]}. + * + *

The term must be a collection of the same type as the column. + * + *

DO NOT USE THIS TO DECREMENT COUNTERS. Use the dedicated {@link + * #decrement(CqlIdentifier, Term)} methods instead. While the operator is technically the same, + * and it would be possible to generate an expression such as {@code counter-=1} with this method, + * a collection removal is idempotent while a counter decrement isn't. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.remove(columnId, + * collectionToRemove))}. + * + * @see Assignment#remove(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments remove( + @NonNull CqlIdentifier columnId, @NonNull Term collectionToRemove) { + return set(Assignment.remove(columnId, collectionToRemove)); + } + + /** + * Shortcut for {@link #remove(CqlIdentifier, Term) remove(CqlIdentifier.fromCql(columnName), + * collectionToRemove)}. + * + * @see Assignment#remove(String, Term) + */ + @NonNull + default UpdateWithAssignments remove( + @NonNull String columnName, @NonNull Term collectionToRemove) { + return remove(CqlIdentifier.fromCql(columnName), collectionToRemove); + } + + /** + * Removes a single element to a list column, as in {@code SET l-=[?]}. + * + *

The term must be of the same type as the column's elements. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.removeListElement(columnId, + * suffix))}. + * + * @see Assignment#removeListElement(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments removeListElement( + @NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return set(Assignment.removeListElement(columnId, suffix)); + } + + /** + * Shortcut for {@link #removeListElement(CqlIdentifier, Term) + * removeListElement(CqlIdentifier.fromCql(columnName), suffix)}. + * + * @see Assignment#removeListElement(String, Term) + */ + @NonNull + default UpdateWithAssignments removeListElement( + @NonNull String columnName, @NonNull Term suffix) { + return removeListElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Removes a single element to a set column, as in {@code SET s-={?}}. + * + *

The term must be of the same type as the column's elements. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.removeSetElement(columnId, + * suffix))}. + * + * @see Assignment#removeSetElement(CqlIdentifier, Term) + */ + @NonNull + default UpdateWithAssignments removeSetElement( + @NonNull CqlIdentifier columnId, @NonNull Term suffix) { + return set(Assignment.removeSetElement(columnId, suffix)); + } + + /** + * Shortcut for {@link #removeSetElement(CqlIdentifier, Term) + * removeSetElement(CqlIdentifier.fromCql(columnName), suffix)}. + */ + @NonNull + default UpdateWithAssignments removeSetElement(@NonNull String columnName, @NonNull Term suffix) { + return removeSetElement(CqlIdentifier.fromCql(columnName), suffix); + } + + /** + * Removes a single entry to a map column, as in {@code SET m-={?:?}}. + * + *

The terms must be of the same type as the column's keys and values respectively. + * + *

This is a shortcut for {@link #set(Assignment) set(Assignment.removeMapEntry(columnId, key, + * value)}. + * + * @see Assignment#removeMapEntry(CqlIdentifier, Term, Term) + */ + @NonNull + default UpdateWithAssignments removeMapEntry( + @NonNull CqlIdentifier columnId, @NonNull Term key, @NonNull Term value) { + return set(Assignment.removeMapEntry(columnId, key, value)); + } + + /** + * Shortcut for {@link #removeMapEntry(CqlIdentifier, Term, Term) + * removeMapEntry(CqlIdentifier.fromCql(columnName), key, value)}. + * + * @see Assignment#removeMapEntry(String, Term, Term) + */ + @NonNull + default UpdateWithAssignments removeMapEntry( + @NonNull String columnName, @NonNull Term key, @NonNull Term value) { + return removeMapEntry(CqlIdentifier.fromCql(columnName), key, value); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/Update.java b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/Update.java new file mode 100644 index 00000000000..5495b910d56 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/Update.java @@ -0,0 +1,27 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.update; + +import com.datastax.oss.driver.api.querybuilder.BuildableQuery; +import com.datastax.oss.driver.api.querybuilder.condition.ConditionalStatement; +import com.datastax.oss.driver.api.querybuilder.relation.OngoingWhereClause; + +/** + * A buildable UPDATE statement that has at least one assignment and one WHERE clause. You can keep + * adding WHERE clauses, or add IF conditions. + */ +public interface Update + extends OngoingWhereClause, ConditionalStatement, BuildableQuery {} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/UpdateStart.java b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/UpdateStart.java new file mode 100644 index 00000000000..e0dd69167e8 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/UpdateStart.java @@ -0,0 +1,44 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.update; + +import com.datastax.oss.driver.api.querybuilder.BindMarker; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * The beginning of an UPDATE statement. It needs at least one assignment before the WHERE clause + * can be added. + */ +public interface UpdateStart extends OngoingAssignment { + + /** + * Adds a USING TIMESTAMP clause to this statement with a literal value. + * + *

If this method or {@link #usingTimestamp(BindMarker)} is called multiple times, the last + * value is used. + */ + @NonNull + UpdateStart usingTimestamp(long timestamp); + + /** + * Adds a USING TIMESTAMP clause to this statement with a bind marker. + * + *

If this method or {@link #usingTimestamp(long)} is called multiple times, the last value is + * used. + */ + @NonNull + UpdateStart usingTimestamp(@NonNull BindMarker bindMarker); +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/UpdateWithAssignments.java b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/UpdateWithAssignments.java new file mode 100644 index 00000000000..7f8559289f0 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/api/querybuilder/update/UpdateWithAssignments.java @@ -0,0 +1,24 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.update; + +import com.datastax.oss.driver.api.querybuilder.relation.OngoingWhereClause; + +/** + * An UPDATE statement that has at least one assignment. You can keep adding assignments, or add + * WHERE clauses to get a buildable statement. + */ +public interface UpdateWithAssignments extends OngoingAssignment, OngoingWhereClause {} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/ArithmeticOperator.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/ArithmeticOperator.java new file mode 100644 index 00000000000..ccb6949a7c5 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/ArithmeticOperator.java @@ -0,0 +1,51 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder; + +import edu.umd.cs.findbugs.annotations.NonNull; + +public enum ArithmeticOperator { + OPPOSITE("-", 2, 2), + PRODUCT("*", 2, 2), + QUOTIENT("/", 2, 3), + REMAINDER("%", 2, 3), + SUM("+", 1, 1), + DIFFERENCE("-", 1, 2), + ; + + private final String symbol; + private final int precedenceLeft; + private final int precedenceRight; + + ArithmeticOperator(String symbol, int precedenceLeft, int precedenceRight) { + this.symbol = symbol; + this.precedenceLeft = precedenceLeft; + this.precedenceRight = precedenceRight; + } + + @NonNull + public String getSymbol() { + return symbol; + } + + public int getPrecedenceLeft() { + return precedenceLeft; + } + + public int getPrecedenceRight() { + return precedenceRight; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/CqlHelper.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/CqlHelper.java new file mode 100644 index 00000000000..6e4117c3856 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/CqlHelper.java @@ -0,0 +1,109 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.CqlSnippet; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Collection; + +public class CqlHelper { + + public static void appendIds( + @NonNull Iterable ids, + @NonNull StringBuilder builder, + @Nullable String prefix, + @NonNull String separator, + @Nullable String suffix) { + boolean first = true; + for (CqlIdentifier id : ids) { + if (first) { + if (prefix != null) { + builder.append(prefix); + } + first = false; + } else { + builder.append(separator); + } + builder.append(id.asCql(true)); + } + if (!first && suffix != null) { + builder.append(suffix); + } + } + + public static void append( + @NonNull Iterable snippets, + @NonNull StringBuilder builder, + @Nullable String prefix, + @NonNull String separator, + @Nullable String suffix) { + boolean first = true; + for (CqlSnippet snippet : snippets) { + if (first) { + if (prefix != null) { + builder.append(prefix); + } + first = false; + } else { + builder.append(separator); + } + snippet.appendTo(builder); + } + if (!first && suffix != null) { + builder.append(suffix); + } + } + + public static void qualify( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier element, + @NonNull StringBuilder builder) { + if (keyspace != null) { + builder.append(keyspace.asCql(true)).append('.'); + } + builder.append(element.asCql(true)); + } + + public static void buildPrimaryKey( + @NonNull Collection partitionKeyColumns, + @NonNull Collection clusteringKeyColumns, + @NonNull StringBuilder builder) { + builder.append("PRIMARY KEY("); + boolean firstKey = true; + + if (partitionKeyColumns.size() > 1) { + builder.append('('); + } + for (CqlIdentifier partitionColumn : partitionKeyColumns) { + if (firstKey) { + firstKey = false; + } else { + builder.append(','); + } + builder.append(partitionColumn.asCql(true)); + } + if (partitionKeyColumns.size() > 1) { + builder.append(')'); + } + + for (CqlIdentifier clusteringColumn : clusteringKeyColumns) { + builder.append(',').append(clusteringColumn.asCql(true)); + } + builder.append(')'); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/DefaultLiteral.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/DefaultLiteral.java new file mode 100644 index 00000000000..e6c08f8e063 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/DefaultLiteral.java @@ -0,0 +1,85 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.querybuilder.Literal; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultLiteral implements Literal { + + private final ValueT value; + private final TypeCodec codec; + private final CqlIdentifier alias; + + public DefaultLiteral(@Nullable ValueT value, @Nullable TypeCodec codec) { + this(value, codec, null); + } + + public DefaultLiteral( + @Nullable ValueT value, @Nullable TypeCodec codec, @Nullable CqlIdentifier alias) { + Preconditions.checkArgument( + value == null || codec != null, "Must provide a codec if the value is not null"); + this.value = value; + this.codec = codec; + this.alias = alias; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + if (value == null) { + builder.append("NULL"); + } else { + builder.append(codec.format(value)); + } + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @Override + public boolean isIdempotent() { + return true; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new DefaultLiteral<>(value, codec, alias); + } + + @Nullable + public ValueT getValue() { + return value; + } + + @Nullable + public TypeCodec getCodec() { + return codec; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/DefaultRaw.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/DefaultRaw.java new file mode 100644 index 00000000000..c60d85d0290 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/DefaultRaw.java @@ -0,0 +1,90 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.Raw; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultRaw implements Raw { + + private final String rawExpression; + private final CqlIdentifier alias; + + public DefaultRaw(@NonNull String rawExpression) { + this(rawExpression, null); + } + + private DefaultRaw(@NonNull String rawExpression, @Nullable CqlIdentifier alias) { + Preconditions.checkNotNull(rawExpression); + this.rawExpression = rawExpression; + this.alias = alias; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new DefaultRaw(rawExpression, alias); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append(rawExpression); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @Override + public boolean isIdempotent() { + return false; + } + + @NonNull + public String getRawExpression() { + return rawExpression; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof DefaultRaw) { + DefaultRaw that = (DefaultRaw) other; + return this.rawExpression.equals(that.rawExpression) + && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(rawExpression, alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/ImmutableCollections.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/ImmutableCollections.java new file mode 100644 index 00000000000..86e7b1e239d --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/ImmutableCollections.java @@ -0,0 +1,91 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import java.util.function.Function; + +public class ImmutableCollections { + + @NonNull + public static ImmutableList append(@NonNull ImmutableList list, @NonNull T newElement) { + return ImmutableList.builder().addAll(list).add(newElement).build(); + } + + @NonNull + public static ImmutableList concat( + @NonNull ImmutableList list1, @NonNull Iterable list2) { + return ImmutableList.builder().addAll(list1).addAll(list2).build(); + } + + @NonNull + public static ImmutableList modifyLast( + @NonNull ImmutableList list, @NonNull Function change) { + ImmutableList.Builder builder = ImmutableList.builder(); + int size = list.size(); + for (int i = 0; i < size - 1; i++) { + builder.add(list.get(i)); + } + builder.add(change.apply(list.get(size - 1))); + return builder.build(); + } + + /** + * If the existing map has an entry with the new key, that old entry will be removed, but the new + * entry will appear last in the iteration order of the resulting map. Example: + * + *

{@code
+   * append({a=>1, b=>2, c=>3}, a, 4) == {b=>2, c=>3, a=>4}
+   * }
+ */ + @NonNull + public static ImmutableMap append( + @NonNull ImmutableMap map, @NonNull K newKey, @NonNull V newValue) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : map.entrySet()) { + if (!entry.getKey().equals(newKey)) { + builder.put(entry); + } + } + builder.put(newKey, newValue); + return builder.build(); + } + + /** + * If the existing map has entries that collide with the new map, those old entries will be + * removed, but the new entries will appear at their new position in the iteration order of the + * resulting map. Example: + * + *
{@code
+   * concat({a=>1, b=>2, c=>3}, {c=>4, a=>5}) == {b=>2, c=>4, a=>5}
+   * }
+ */ + @NonNull + public static ImmutableMap concat( + @NonNull ImmutableMap map1, @NonNull Map map2) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (Map.Entry entry : map1.entrySet()) { + if (!map2.containsKey(entry.getKey())) { + builder.put(entry); + } + } + builder.putAll(map2); + return builder.build(); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/condition/DefaultCondition.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/condition/DefaultCondition.java new file mode 100644 index 00000000000..8fa66674b79 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/condition/DefaultCondition.java @@ -0,0 +1,62 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.condition; + +import com.datastax.oss.driver.api.querybuilder.condition.Condition; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.lhs.LeftOperand; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultCondition implements Condition { + + private final LeftOperand leftOperand; + private final String operator; + private final Term rightOperand; + + public DefaultCondition( + @NonNull LeftOperand leftOperand, @NonNull String operator, @Nullable Term rightOperand) { + this.leftOperand = leftOperand; + this.operator = operator; + this.rightOperand = rightOperand; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + leftOperand.appendTo(builder); + builder.append(operator); + if (rightOperand != null) { + rightOperand.appendTo(builder); + } + } + + @NonNull + public LeftOperand getLeftOperand() { + return leftOperand; + } + + @NonNull + public String getOperator() { + return operator; + } + + @Nullable + public Term getRightOperand() { + return rightOperand; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/condition/DefaultConditionBuilder.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/condition/DefaultConditionBuilder.java new file mode 100644 index 00000000000..3f3561cfe45 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/condition/DefaultConditionBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.condition; + +import com.datastax.oss.driver.api.querybuilder.condition.Condition; +import com.datastax.oss.driver.api.querybuilder.condition.ConditionBuilder; +import com.datastax.oss.driver.api.querybuilder.condition.ConditionalStatement; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.lhs.LeftOperand; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultConditionBuilder implements ConditionBuilder { + + private final LeftOperand leftOperand; + + public DefaultConditionBuilder(@NonNull LeftOperand leftOperand) { + this.leftOperand = leftOperand; + } + + @NonNull + @Override + public Condition build(@NonNull String operator, @Nullable Term rightOperand) { + return new DefaultCondition(leftOperand, operator, rightOperand); + } + + @Immutable + public static class Fluent> + implements ConditionBuilder { + + private final ConditionalStatement statement; + private final ConditionBuilder delegate; + + public Fluent( + @NonNull ConditionalStatement statement, @NonNull LeftOperand leftOperand) { + this.statement = statement; + this.delegate = new DefaultConditionBuilder(leftOperand); + } + + @NonNull + @Override + public StatementT build(@NonNull String operator, @Nullable Term rightOperand) { + return statement.if_(delegate.build(operator, rightOperand)); + } + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/delete/DefaultDelete.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/delete/DefaultDelete.java new file mode 100644 index 00000000000..319acf238a9 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/delete/DefaultDelete.java @@ -0,0 +1,250 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.delete; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder; +import com.datastax.oss.driver.api.querybuilder.BindMarker; +import com.datastax.oss.driver.api.querybuilder.condition.Condition; +import com.datastax.oss.driver.api.querybuilder.delete.Delete; +import com.datastax.oss.driver.api.querybuilder.delete.DeleteSelection; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.internal.querybuilder.select.ElementSelector; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultDelete implements DeleteSelection, Delete { + + private final CqlIdentifier keyspace; + private final CqlIdentifier table; + private final ImmutableList selectors; + private final ImmutableList relations; + private final Object timestamp; + private final boolean ifExists; + private final ImmutableList conditions; + + public DefaultDelete(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) { + this(keyspace, table, ImmutableList.of(), ImmutableList.of(), null, false, ImmutableList.of()); + } + + public DefaultDelete( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier table, + @NonNull ImmutableList selectors, + @NonNull ImmutableList relations, + @Nullable Object timestamp, + boolean ifExists, + @NonNull ImmutableList conditions) { + this.keyspace = keyspace; + this.table = table; + this.selectors = selectors; + this.relations = relations; + this.timestamp = timestamp; + this.ifExists = ifExists; + this.conditions = conditions; + } + + @NonNull + @Override + public DeleteSelection selector(@NonNull Selector selector) { + return withSelectors(ImmutableCollections.append(selectors, selector)); + } + + @NonNull + @Override + public DeleteSelection selectors(@NonNull Iterable additionalSelectors) { + return withSelectors(ImmutableCollections.concat(selectors, additionalSelectors)); + } + + @NonNull + public DeleteSelection withSelectors(@NonNull ImmutableList newSelectors) { + return new DefaultDelete( + keyspace, table, newSelectors, relations, timestamp, ifExists, conditions); + } + + @NonNull + @Override + public Delete where(@NonNull Relation relation) { + return withRelations(ImmutableCollections.append(relations, relation)); + } + + @NonNull + @Override + public Delete where(@NonNull Iterable additionalRelations) { + return withRelations(ImmutableCollections.concat(relations, additionalRelations)); + } + + @NonNull + public Delete withRelations(@NonNull ImmutableList newRelations) { + return new DefaultDelete( + keyspace, table, selectors, newRelations, timestamp, ifExists, conditions); + } + + @NonNull + @Override + public DeleteSelection usingTimestamp(long newTimestamp) { + return new DefaultDelete( + keyspace, table, selectors, relations, newTimestamp, ifExists, conditions); + } + + @NonNull + @Override + public DeleteSelection usingTimestamp(@Nullable BindMarker newTimestamp) { + return new DefaultDelete( + keyspace, table, selectors, relations, newTimestamp, ifExists, conditions); + } + + @NonNull + @Override + public Delete ifExists() { + return new DefaultDelete( + keyspace, table, selectors, relations, timestamp, true, ImmutableList.of()); + } + + @NonNull + @Override + public Delete if_(@NonNull Condition condition) { + return withConditions(ImmutableCollections.append(conditions, condition)); + } + + @NonNull + @Override + public Delete if_(@NonNull Iterable additionalConditions) { + return withConditions(ImmutableCollections.concat(conditions, additionalConditions)); + } + + @NonNull + public Delete withConditions(@NonNull ImmutableList newConditions) { + return new DefaultDelete( + keyspace, table, selectors, relations, timestamp, false, newConditions); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("DELETE"); + + CqlHelper.append(selectors, builder, " ", ",", null); + + builder.append(" FROM "); + CqlHelper.qualify(keyspace, table, builder); + + if (timestamp != null) { + builder.append(" USING TIMESTAMP "); + if (timestamp instanceof BindMarker) { + ((BindMarker) timestamp).appendTo(builder); + } else { + builder.append(timestamp); + } + } + + CqlHelper.append(relations, builder, " WHERE ", " AND ", null); + + if (ifExists) { + builder.append(" IF EXISTS"); + } else { + CqlHelper.append(conditions, builder, " IF ", " AND ", null); + } + return builder.toString(); + } + + @NonNull + @Override + public SimpleStatement build() { + return builder().build(); + } + + @NonNull + @Override + public SimpleStatement build(@NonNull Object... values) { + return builder().addPositionalValues(values).build(); + } + + @NonNull + @Override + public SimpleStatement build(@NonNull Map namedValues) { + SimpleStatementBuilder builder = builder(); + for (Map.Entry entry : namedValues.entrySet()) { + builder.addNamedValue(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + + @NonNull + @Override + public SimpleStatementBuilder builder() { + return SimpleStatement.builder(asCql()).setIdempotence(isIdempotent()); + } + + public boolean isIdempotent() { + // Conditional queries are never idempotent, see JAVA-819 + if (!conditions.isEmpty() || ifExists) { + return false; + } else { + for (Selector selector : selectors) { + // `DELETE list[0]` is not idempotent. Unfortunately we don't know what type of collection + // an elements selector targets, so be conservative. + if (selector instanceof ElementSelector) { + return false; + } + } + for (Relation relation : relations) { + if (!relation.isIdempotent()) { + return false; + } + } + return true; + } + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getTable() { + return table; + } + + @NonNull + public ImmutableList getSelectors() { + return selectors; + } + + @NonNull + public ImmutableList getRelations() { + return relations; + } + + @Nullable + public Object getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return asCql(); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/insert/DefaultInsert.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/insert/DefaultInsert.java new file mode 100644 index 00000000000..0e51c316dc5 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/insert/DefaultInsert.java @@ -0,0 +1,278 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.insert; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.querybuilder.BindMarker; +import com.datastax.oss.driver.api.querybuilder.QueryBuilder; +import com.datastax.oss.driver.api.querybuilder.insert.Insert; +import com.datastax.oss.driver.api.querybuilder.insert.InsertInto; +import com.datastax.oss.driver.api.querybuilder.insert.JsonInsert; +import com.datastax.oss.driver.api.querybuilder.insert.RegularInsert; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultInsert implements InsertInto, RegularInsert, JsonInsert { + + public enum MissingJsonBehavior { + NULL, + UNSET + } + + private final CqlIdentifier keyspace; + private final CqlIdentifier table; + private final Term json; + private final MissingJsonBehavior missingJsonBehavior; + private final ImmutableMap assignments; + private final Object timestamp; + private final boolean ifNotExists; + + public DefaultInsert(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) { + this(keyspace, table, null, null, ImmutableMap.of(), null, false); + } + + public DefaultInsert( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier table, + @Nullable Term json, + @Nullable MissingJsonBehavior missingJsonBehavior, + @NonNull ImmutableMap assignments, + @Nullable Object timestamp, + boolean ifNotExists) { + // Note: the public API guarantees this, but check in case someone is calling the internal API + // directly. + Preconditions.checkArgument( + json == null || assignments.isEmpty(), "JSON insert can't have regular assignments"); + this.keyspace = keyspace; + this.table = table; + this.json = json; + this.missingJsonBehavior = missingJsonBehavior; + this.assignments = assignments; + this.timestamp = timestamp; + this.ifNotExists = ifNotExists; + } + + @NonNull + @Override + public JsonInsert json(@NonNull String json) { + return new DefaultInsert( + keyspace, + table, + QueryBuilder.literal(json), + missingJsonBehavior, + ImmutableMap.of(), + timestamp, + ifNotExists); + } + + @NonNull + @Override + public JsonInsert json(@NonNull BindMarker json) { + return new DefaultInsert( + keyspace, table, json, missingJsonBehavior, ImmutableMap.of(), timestamp, ifNotExists); + } + + @NonNull + @Override + public JsonInsert json(@NonNull T value, @NonNull TypeCodec codec) { + return new DefaultInsert( + keyspace, + table, + QueryBuilder.literal(value, codec), + missingJsonBehavior, + ImmutableMap.of(), + timestamp, + ifNotExists); + } + + @NonNull + @Override + public JsonInsert defaultNull() { + return new DefaultInsert( + keyspace, table, json, MissingJsonBehavior.NULL, ImmutableMap.of(), timestamp, ifNotExists); + } + + @NonNull + @Override + public JsonInsert defaultUnset() { + return new DefaultInsert( + keyspace, + table, + json, + MissingJsonBehavior.UNSET, + ImmutableMap.of(), + timestamp, + ifNotExists); + } + + @NonNull + @Override + public RegularInsert value(@NonNull CqlIdentifier columnId, @NonNull Term value) { + return new DefaultInsert( + keyspace, + table, + null, + null, + ImmutableCollections.append(assignments, columnId, value), + timestamp, + ifNotExists); + } + + @NonNull + @Override + public Insert ifNotExists() { + return new DefaultInsert( + keyspace, table, json, missingJsonBehavior, assignments, timestamp, true); + } + + @NonNull + @Override + public Insert usingTimestamp(long timestamp) { + return new DefaultInsert( + keyspace, table, json, missingJsonBehavior, assignments, timestamp, ifNotExists); + } + + @NonNull + @Override + public Insert usingTimestamp(@Nullable BindMarker timestamp) { + return new DefaultInsert( + keyspace, table, json, missingJsonBehavior, assignments, timestamp, ifNotExists); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("INSERT INTO "); + CqlHelper.qualify(keyspace, table, builder); + + if (json == null) { + CqlHelper.appendIds(assignments.keySet(), builder, " (", ",", ")"); + CqlHelper.append(assignments.values(), builder, " VALUES (", ",", ")"); + } else { + builder.append(" JSON "); + json.appendTo(builder); + if (missingJsonBehavior == MissingJsonBehavior.NULL) { + builder.append(" DEFAULT NULL"); + } else if (missingJsonBehavior == MissingJsonBehavior.UNSET) { + builder.append(" DEFAULT UNSET"); + } + } + if (ifNotExists) { + builder.append(" IF NOT EXISTS"); + } + if (timestamp != null) { + builder.append(" USING TIMESTAMP "); + if (timestamp instanceof BindMarker) { + ((BindMarker) timestamp).appendTo(builder); + } else { + builder.append(timestamp); + } + } + return builder.toString(); + } + + @NonNull + @Override + public SimpleStatement build() { + return builder().build(); + } + + @NonNull + @Override + public SimpleStatement build(@NonNull Object... values) { + return builder().addPositionalValues(values).build(); + } + + @NonNull + @Override + public SimpleStatement build(@NonNull Map namedValues) { + SimpleStatementBuilder builder = builder(); + for (Map.Entry entry : namedValues.entrySet()) { + builder.addNamedValue(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + + @NonNull + @Override + public SimpleStatementBuilder builder() { + return SimpleStatement.builder(asCql()).setIdempotence(isIdempotent()); + } + + public boolean isIdempotent() { + // Conditional queries are never idempotent, see JAVA-819 + if (ifNotExists) { + return false; + } else { + for (Term value : assignments.values()) { + if (!value.isIdempotent()) { + return false; + } + } + return true; + } + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getTable() { + return table; + } + + @Nullable + public Object getJson() { + return json; + } + + @Nullable + public MissingJsonBehavior getMissingJsonBehavior() { + return missingJsonBehavior; + } + + @NonNull + public ImmutableMap getAssignments() { + return assignments; + } + + @Nullable + public Object getTimestamp() { + return timestamp; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + @Override + public String toString() { + return asCql(); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/ColumnComponentLeftOperand.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/ColumnComponentLeftOperand.java new file mode 100644 index 00000000000..e3bd2a641f4 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/ColumnComponentLeftOperand.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.lhs; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class ColumnComponentLeftOperand implements LeftOperand { + + private final CqlIdentifier columnId; + private final Term index; + + public ColumnComponentLeftOperand(@NonNull CqlIdentifier columnId, @NonNull Term index) { + this.columnId = columnId; + this.index = index; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append(columnId.asCql(true)).append('['); + index.appendTo(builder); + builder.append(']'); + } + + @NonNull + public CqlIdentifier getColumnId() { + return columnId; + } + + @NonNull + public Term getIndex() { + return index; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/ColumnLeftOperand.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/ColumnLeftOperand.java new file mode 100644 index 00000000000..d4d79474bac --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/ColumnLeftOperand.java @@ -0,0 +1,40 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.lhs; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class ColumnLeftOperand implements LeftOperand { + + private final CqlIdentifier columnId; + + public ColumnLeftOperand(@NonNull CqlIdentifier columnId) { + this.columnId = columnId; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append(columnId.asCql(true)); + } + + @NonNull + public CqlIdentifier getColumnId() { + return columnId; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/FieldLeftOperand.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/FieldLeftOperand.java new file mode 100644 index 00000000000..76818b737f5 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/FieldLeftOperand.java @@ -0,0 +1,47 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.lhs; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class FieldLeftOperand implements LeftOperand { + + private final CqlIdentifier columnId; + private final CqlIdentifier fieldId; + + public FieldLeftOperand(@NonNull CqlIdentifier columnId, @NonNull CqlIdentifier fieldId) { + this.columnId = columnId; + this.fieldId = fieldId; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append(columnId.asCql(true)).append('.').append(fieldId.asCql(true)); + } + + @NonNull + public CqlIdentifier getColumnId() { + return columnId; + } + + @NonNull + public CqlIdentifier getFieldId() { + return fieldId; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/LeftOperand.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/LeftOperand.java new file mode 100644 index 00000000000..fb5cdce0824 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/LeftOperand.java @@ -0,0 +1,32 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.lhs; + +import com.datastax.oss.driver.api.querybuilder.CqlSnippet; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.internal.querybuilder.condition.DefaultCondition; +import com.datastax.oss.driver.internal.querybuilder.relation.DefaultRelation; + +/** + * The left operand of a relation. + * + *

Doesn't need to be in an API package since it's only used internally by {@link + * DefaultRelation} and {@link DefaultCondition}. + * + *

Implementations of this interface are only used temporarily while building a {@link Relation}, + * so they don't need to provide introspection (i.e. public getters). + */ +public interface LeftOperand extends CqlSnippet {} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/TokenLeftOperand.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/TokenLeftOperand.java new file mode 100644 index 00000000000..986f701fddf --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/TokenLeftOperand.java @@ -0,0 +1,40 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.lhs; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class TokenLeftOperand implements LeftOperand { + + private final Iterable identifiers; + + public TokenLeftOperand(@NonNull Iterable identifiers) { + this.identifiers = identifiers; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + CqlHelper.appendIds(identifiers, builder, "token(", ",", ")"); + } + + public @NonNull Iterable getIdentifiers() { + return identifiers; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/TupleLeftOperand.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/TupleLeftOperand.java new file mode 100644 index 00000000000..82ce28ffcc5 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/lhs/TupleLeftOperand.java @@ -0,0 +1,41 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.lhs; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class TupleLeftOperand implements LeftOperand { + + private final Iterable identifiers; + + public TupleLeftOperand(@NonNull Iterable identifiers) { + this.identifiers = identifiers; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + CqlHelper.appendIds(identifiers, builder, "(", ",", ")"); + } + + @NonNull + public Iterable getIdentifiers() { + return identifiers; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/CustomIndexRelation.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/CustomIndexRelation.java new file mode 100644 index 00000000000..0ed15a5f805 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/CustomIndexRelation.java @@ -0,0 +1,56 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.relation; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class CustomIndexRelation implements Relation { + + private final CqlIdentifier indexId; + private final Term expression; + + public CustomIndexRelation(@NonNull CqlIdentifier indexId, @NonNull Term expression) { + this.indexId = indexId; + this.expression = expression; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append("expr(").append(indexId.asCql(true)).append(','); + expression.appendTo(builder); + builder.append(')'); + } + + @Override + public boolean isIdempotent() { + return false; + } + + @NonNull + public CqlIdentifier getIndexId() { + return indexId; + } + + @NonNull + public Term getExpression() { + return expression; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultColumnComponentRelationBuilder.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultColumnComponentRelationBuilder.java new file mode 100644 index 00000000000..63a97a831db --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultColumnComponentRelationBuilder.java @@ -0,0 +1,69 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.relation; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.relation.ColumnComponentRelationBuilder; +import com.datastax.oss.driver.api.querybuilder.relation.OngoingWhereClause; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.lhs.ColumnComponentLeftOperand; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultColumnComponentRelationBuilder + implements ColumnComponentRelationBuilder { + + private final CqlIdentifier columnId; + private final Term index; + + public DefaultColumnComponentRelationBuilder( + @NonNull CqlIdentifier columnId, @NonNull Term index) { + this.columnId = columnId; + this.index = index; + } + + @NonNull + @Override + public Relation build(@NonNull String operator, @Nullable Term rightOperand) { + return new DefaultRelation( + new ColumnComponentLeftOperand(columnId, index), operator, rightOperand); + } + + @Immutable + public static class Fluent> + implements ColumnComponentRelationBuilder { + + private final OngoingWhereClause statement; + private final ColumnComponentRelationBuilder delegate; + + public Fluent( + @NonNull OngoingWhereClause statement, + @NonNull CqlIdentifier columnId, + @NonNull Term index) { + this.statement = statement; + this.delegate = new DefaultColumnComponentRelationBuilder(columnId, index); + } + + @NonNull + @Override + public StatementT build(@NonNull String operator, @Nullable Term rightOperand) { + return statement.where(delegate.build(operator, rightOperand)); + } + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultColumnRelationBuilder.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultColumnRelationBuilder.java new file mode 100644 index 00000000000..65ac9f1fba2 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultColumnRelationBuilder.java @@ -0,0 +1,64 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.relation; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.relation.ColumnRelationBuilder; +import com.datastax.oss.driver.api.querybuilder.relation.OngoingWhereClause; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.lhs.ColumnLeftOperand; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultColumnRelationBuilder implements ColumnRelationBuilder { + + private final CqlIdentifier columnId; + + public DefaultColumnRelationBuilder(@NonNull CqlIdentifier columnId) { + Preconditions.checkNotNull(columnId); + this.columnId = columnId; + } + + @NonNull + @Override + public Relation build(@NonNull String operator, @Nullable Term rightOperand) { + return new DefaultRelation(new ColumnLeftOperand(columnId), operator, rightOperand); + } + + @Immutable + public static class Fluent> + implements ColumnRelationBuilder { + + private final OngoingWhereClause statement; + private final ColumnRelationBuilder delegate; + + public Fluent( + @NonNull OngoingWhereClause statement, @NonNull CqlIdentifier columnId) { + this.statement = statement; + this.delegate = new DefaultColumnRelationBuilder(columnId); + } + + @NonNull + @Override + public StatementT build(@NonNull String operator, @Nullable Term rightOperand) { + return statement.where(delegate.build(operator, rightOperand)); + } + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultMultiColumnRelationBuilder.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultMultiColumnRelationBuilder.java new file mode 100644 index 00000000000..5f66441313f --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultMultiColumnRelationBuilder.java @@ -0,0 +1,67 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.relation; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.relation.MultiColumnRelationBuilder; +import com.datastax.oss.driver.api.querybuilder.relation.OngoingWhereClause; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.lhs.TupleLeftOperand; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultMultiColumnRelationBuilder implements MultiColumnRelationBuilder { + + private final Iterable identifiers; + + public DefaultMultiColumnRelationBuilder(@NonNull Iterable identifiers) { + Preconditions.checkNotNull(identifiers); + Preconditions.checkArgument( + identifiers.iterator().hasNext(), "Tuple must contain at least one column"); + this.identifiers = identifiers; + } + + @NonNull + @Override + public Relation build(@NonNull String operator, @Nullable Term rightOperand) { + return new DefaultRelation(new TupleLeftOperand(identifiers), operator, rightOperand); + } + + @Immutable + public static class Fluent> + implements MultiColumnRelationBuilder { + + private final OngoingWhereClause statement; + private final MultiColumnRelationBuilder delegate; + + public Fluent( + @NonNull OngoingWhereClause statement, + @NonNull Iterable identifiers) { + this.statement = statement; + this.delegate = new DefaultMultiColumnRelationBuilder(identifiers); + } + + @NonNull + @Override + public StatementT build(@NonNull String operator, @Nullable Term rightOperand) { + return statement.where(delegate.build(operator, rightOperand)); + } + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultRelation.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultRelation.java new file mode 100644 index 00000000000..3807986e611 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultRelation.java @@ -0,0 +1,70 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.relation; + +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.lhs.LeftOperand; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultRelation implements Relation { + + private final LeftOperand leftOperand; + private final String operator; + private final Term rightOperand; + + public DefaultRelation( + @NonNull LeftOperand leftOperand, @NonNull String operator, @Nullable Term rightOperand) { + Preconditions.checkNotNull(leftOperand); + Preconditions.checkNotNull(operator); + this.leftOperand = leftOperand; + this.operator = operator; + this.rightOperand = rightOperand; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + leftOperand.appendTo(builder); + builder.append(operator); + if (rightOperand != null) { + rightOperand.appendTo(builder); + } + } + + @Override + public boolean isIdempotent() { + return rightOperand.isIdempotent(); + } + + @NonNull + public LeftOperand getLeftOperand() { + return leftOperand; + } + + @NonNull + public String getOperator() { + return operator; + } + + @Nullable + public Term getRightOperand() { + return rightOperand; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultTokenRelationBuilder.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultTokenRelationBuilder.java new file mode 100644 index 00000000000..fb841d39a73 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/relation/DefaultTokenRelationBuilder.java @@ -0,0 +1,63 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.relation; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.relation.OngoingWhereClause; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.relation.TokenRelationBuilder; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.lhs.TokenLeftOperand; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultTokenRelationBuilder implements TokenRelationBuilder { + + private final Iterable identifiers; + + public DefaultTokenRelationBuilder(@NonNull Iterable identifiers) { + this.identifiers = identifiers; + } + + @NonNull + @Override + public Relation build(@NonNull String operator, @Nullable Term rightOperand) { + return new DefaultRelation(new TokenLeftOperand(identifiers), operator, rightOperand); + } + + @Immutable + public static class Fluent> + implements TokenRelationBuilder { + + private final OngoingWhereClause statement; + private final TokenRelationBuilder delegate; + + public Fluent( + @NonNull OngoingWhereClause statement, + @NonNull Iterable identifiers) { + this.statement = statement; + this.delegate = new DefaultTokenRelationBuilder(identifiers); + } + + @NonNull + @Override + public StatementT build(@NonNull String operator, @Nullable Term rightOperand) { + return statement.where(delegate.build(operator, rightOperand)); + } + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterKeyspace.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterKeyspace.java new file mode 100644 index 00000000000..3e35311cfaf --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterKeyspace.java @@ -0,0 +1,77 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.schema.AlterKeyspace; +import com.datastax.oss.driver.api.querybuilder.schema.AlterKeyspaceStart; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultAlterKeyspace implements AlterKeyspaceStart, AlterKeyspace { + + private final CqlIdentifier keyspaceName; + private final ImmutableMap options; + + public DefaultAlterKeyspace(@NonNull CqlIdentifier keyspaceName) { + this(keyspaceName, ImmutableMap.of()); + } + + public DefaultAlterKeyspace( + @NonNull CqlIdentifier keyspaceName, @NonNull ImmutableMap options) { + this.keyspaceName = keyspaceName; + this.options = options; + } + + @NonNull + @Override + public AlterKeyspace withReplicationOptions(@NonNull Map replicationOptions) { + return withOption("replication", replicationOptions); + } + + @NonNull + @Override + public AlterKeyspace withOption(@NonNull String name, @NonNull Object value) { + return new DefaultAlterKeyspace( + keyspaceName, ImmutableCollections.append(options, name, value)); + } + + @NonNull + @Override + public String asCql() { + return "ALTER KEYSPACE " + keyspaceName.asCql(true) + OptionsUtils.buildOptions(options, true); + } + + @NonNull + @Override + public Map getOptions() { + return options; + } + + @NonNull + public CqlIdentifier getKeyspace() { + return keyspaceName; + } + + @Override + public String toString() { + return asCql(); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterMaterializedView.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterMaterializedView.java new file mode 100644 index 00000000000..d32f38ba4ce --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterMaterializedView.java @@ -0,0 +1,92 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.schema.AlterMaterializedView; +import com.datastax.oss.driver.api.querybuilder.schema.AlterMaterializedViewStart; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultAlterMaterializedView + implements AlterMaterializedViewStart, AlterMaterializedView { + + private final CqlIdentifier keyspace; + private final CqlIdentifier viewName; + + private final ImmutableMap options; + + public DefaultAlterMaterializedView(@NonNull CqlIdentifier viewName) { + this(null, viewName); + } + + public DefaultAlterMaterializedView( + @Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier viewName) { + this(keyspace, viewName, ImmutableMap.of()); + } + + public DefaultAlterMaterializedView( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier viewName, + @NonNull ImmutableMap options) { + this.keyspace = keyspace; + this.viewName = viewName; + this.options = options; + } + + @NonNull + @Override + public AlterMaterializedView withOption(@NonNull String name, @NonNull Object value) { + return new DefaultAlterMaterializedView( + keyspace, viewName, ImmutableCollections.append(options, name, value)); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("ALTER MATERIALIZED VIEW "); + CqlHelper.qualify(keyspace, viewName, builder); + builder.append(OptionsUtils.buildOptions(options, true)); + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @NonNull + @Override + public Map getOptions() { + return options; + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getMaterializedView() { + return viewName; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterTable.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterTable.java new file mode 100644 index 00000000000..d575ced177b --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterTable.java @@ -0,0 +1,364 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import static com.datastax.oss.driver.internal.querybuilder.schema.Utils.appendSet; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.BuildableQuery; +import com.datastax.oss.driver.api.querybuilder.schema.AlterTableAddColumnEnd; +import com.datastax.oss.driver.api.querybuilder.schema.AlterTableDropColumnEnd; +import com.datastax.oss.driver.api.querybuilder.schema.AlterTableRenameColumnEnd; +import com.datastax.oss.driver.api.querybuilder.schema.AlterTableStart; +import com.datastax.oss.driver.api.querybuilder.schema.AlterTableWithOptionsEnd; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultAlterTable + implements AlterTableStart, + AlterTableAddColumnEnd, + AlterTableDropColumnEnd, + AlterTableRenameColumnEnd, + AlterTableWithOptionsEnd, + BuildableQuery { + + private final CqlIdentifier keyspace; + private final CqlIdentifier tableName; + + private final ImmutableMap columnsToAddInOrder; + private final ImmutableSet columnsToAdd; + private final ImmutableSet columnsToAddStatic; + private final ImmutableSet columnsToDrop; + private final ImmutableMap columnsToRename; + private final CqlIdentifier columnToAlter; + private final DataType columnToAlterType; + private final ImmutableMap options; + private final boolean dropCompactStorage; + + public DefaultAlterTable(@NonNull CqlIdentifier tableName) { + this(null, tableName); + } + + public DefaultAlterTable(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier tableName) { + this( + keyspace, + tableName, + false, + ImmutableMap.of(), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableMap.of(), + null, + null, + ImmutableMap.of()); + } + + public DefaultAlterTable( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier tableName, + boolean dropCompactStorage, + @NonNull ImmutableMap columnsToAddInOrder, + @NonNull ImmutableSet columnsToAdd, + @NonNull ImmutableSet columnsToAddStatic, + @NonNull ImmutableSet columnsToDrop, + @NonNull ImmutableMap columnsToRename, + @Nullable CqlIdentifier columnToAlter, + @Nullable DataType columnToAlterType, + @NonNull ImmutableMap options) { + this.keyspace = keyspace; + this.tableName = tableName; + this.dropCompactStorage = dropCompactStorage; + this.columnsToAddInOrder = columnsToAddInOrder; + this.columnsToAdd = columnsToAdd; + this.columnsToAddStatic = columnsToAddStatic; + this.columnsToDrop = columnsToDrop; + this.columnsToRename = columnsToRename; + this.columnToAlter = columnToAlter; + this.columnToAlterType = columnToAlterType; + this.options = options; + } + + @NonNull + @Override + public AlterTableAddColumnEnd addColumn( + @NonNull CqlIdentifier columnName, @NonNull DataType dataType) { + return new DefaultAlterTable( + keyspace, + tableName, + dropCompactStorage, + ImmutableCollections.append(columnsToAddInOrder, columnName, dataType), + appendSet(columnsToAdd, columnName), + columnsToAddStatic, + columnsToDrop, + columnsToRename, + columnToAlter, + columnToAlterType, + options); + } + + @NonNull + @Override + public AlterTableAddColumnEnd addStaticColumn( + @NonNull CqlIdentifier columnName, @NonNull DataType dataType) { + return new DefaultAlterTable( + keyspace, + tableName, + dropCompactStorage, + ImmutableCollections.append(columnsToAddInOrder, columnName, dataType), + columnsToAdd, + appendSet(columnsToAddStatic, columnName), + columnsToDrop, + columnsToRename, + columnToAlter, + columnToAlterType, + options); + } + + @NonNull + @Override + public BuildableQuery dropCompactStorage() { + return new DefaultAlterTable( + keyspace, + tableName, + true, + columnsToAddInOrder, + columnsToAdd, + columnsToAddStatic, + columnsToDrop, + columnsToRename, + columnToAlter, + columnToAlterType, + options); + } + + @NonNull + @Override + public AlterTableDropColumnEnd dropColumns(@NonNull CqlIdentifier... columnNames) { + ImmutableSet.Builder builder = + ImmutableSet.builder().addAll(columnsToDrop); + for (CqlIdentifier columnName : columnNames) { + builder = builder.add(columnName); + } + + return new DefaultAlterTable( + keyspace, + tableName, + dropCompactStorage, + columnsToAddInOrder, + columnsToAdd, + columnsToAddStatic, + builder.build(), + columnsToRename, + columnToAlter, + columnToAlterType, + options); + } + + @NonNull + @Override + public AlterTableRenameColumnEnd renameColumn( + @NonNull CqlIdentifier from, @NonNull CqlIdentifier to) { + return new DefaultAlterTable( + keyspace, + tableName, + dropCompactStorage, + columnsToAddInOrder, + columnsToAdd, + columnsToAddStatic, + columnsToDrop, + ImmutableCollections.append(columnsToRename, from, to), + columnToAlter, + columnToAlterType, + options); + } + + @NonNull + @Override + public BuildableQuery alterColumn(@NonNull CqlIdentifier columnName, @NonNull DataType dataType) { + return new DefaultAlterTable( + keyspace, + tableName, + dropCompactStorage, + columnsToAddInOrder, + columnsToAdd, + columnsToAddStatic, + columnsToDrop, + columnsToRename, + columnName, + dataType, + options); + } + + @NonNull + @Override + public AlterTableWithOptionsEnd withOption(@NonNull String name, @NonNull Object value) { + return new DefaultAlterTable( + keyspace, + tableName, + dropCompactStorage, + columnsToAddInOrder, + columnsToAdd, + columnsToAddStatic, + columnsToDrop, + columnsToRename, + columnToAlter, + columnToAlterType, + ImmutableCollections.append(options, name, value)); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("ALTER TABLE "); + + CqlHelper.qualify(keyspace, tableName, builder); + + if (columnToAlter != null) { + return builder + .append(" ALTER ") + .append(columnToAlter.asCql(true)) + .append(" TYPE ") + .append(columnToAlterType.asCql(true, true)) + .toString(); + } else if (!columnsToAdd.isEmpty()) { + builder.append(" ADD "); + if (columnsToAdd.size() > 1) { + builder.append('('); + } + boolean first = true; + for (Map.Entry column : columnsToAddInOrder.entrySet()) { + if (first) { + first = false; + } else { + builder.append(','); + } + builder + .append(column.getKey().asCql(true)) + .append(' ') + .append(column.getValue().asCql(true, true)); + + if (columnsToAddStatic.contains(column.getKey())) { + builder.append(" STATIC"); + } + } + if (columnsToAdd.size() > 1) { + builder.append(')'); + } + return builder.toString(); + } else if (!columnsToDrop.isEmpty()) { + boolean moreThanOneDrop = columnsToDrop.size() > 1; + CqlHelper.appendIds( + columnsToDrop, + builder, + moreThanOneDrop ? " DROP (" : " DROP ", + ",", + moreThanOneDrop ? ")" : ""); + return builder.toString(); + } else if (!columnsToRename.isEmpty()) { + builder.append(" RENAME "); + boolean first = true; + for (Map.Entry entry : columnsToRename.entrySet()) { + if (first) { + first = false; + } else { + builder.append(" AND "); + } + builder + .append(entry.getKey().asCql(true)) + .append(" TO ") + .append(entry.getValue().asCql(true)); + } + return builder.toString(); + } else if (dropCompactStorage) { + return builder.append(" DROP COMPACT STORAGE").toString(); + } else if (!options.isEmpty()) { + return builder.append(OptionsUtils.buildOptions(options, true)).toString(); + } + + // While this is incomplete, we should return partially build query at this point for toString + // purposes. + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @NonNull + @Override + public Map getOptions() { + return options; + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getTable() { + return tableName; + } + + @NonNull + public ImmutableMap getColumnsToAddInOrder() { + return columnsToAddInOrder; + } + + @NonNull + public ImmutableSet getColumnsToAddRegular() { + return columnsToAdd; + } + + @NonNull + public ImmutableSet getColumnsToAddStatic() { + return columnsToAddStatic; + } + + @NonNull + public ImmutableSet getColumnsToDrop() { + return columnsToDrop; + } + + @NonNull + public ImmutableMap getColumnsToRename() { + return columnsToRename; + } + + @Nullable + public CqlIdentifier getColumnToAlter() { + return columnToAlter; + } + + @Nullable + public DataType getColumnToAlterType() { + return columnToAlterType; + } + + public boolean isDropCompactStorage() { + return dropCompactStorage; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterType.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterType.java new file mode 100644 index 00000000000..7a75adf0414 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultAlterType.java @@ -0,0 +1,182 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.BuildableQuery; +import com.datastax.oss.driver.api.querybuilder.schema.AlterTypeRenameField; +import com.datastax.oss.driver.api.querybuilder.schema.AlterTypeRenameFieldEnd; +import com.datastax.oss.driver.api.querybuilder.schema.AlterTypeStart; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultAlterType + implements AlterTypeStart, AlterTypeRenameField, AlterTypeRenameFieldEnd, BuildableQuery { + + private final CqlIdentifier keyspace; + private final CqlIdentifier typeName; + + private final CqlIdentifier fieldToAdd; + private final DataType fieldToAddType; + + private final ImmutableMap fieldsToRename; + + private final CqlIdentifier fieldToAlter; + private final DataType fieldToAlterType; + + public DefaultAlterType(@NonNull CqlIdentifier typeName) { + this(null, typeName); + } + + public DefaultAlterType(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier typeName) { + this(keyspace, typeName, null, null, ImmutableMap.of(), null, null); + } + + public DefaultAlterType( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier typeName, + @Nullable CqlIdentifier fieldToAdd, + @Nullable DataType fieldToAddType, + @NonNull ImmutableMap fieldsToRename, + @Nullable CqlIdentifier fieldToAlter, + @Nullable DataType fieldToAlterType) { + this.keyspace = keyspace; + this.typeName = typeName; + this.fieldToAdd = fieldToAdd; + this.fieldToAddType = fieldToAddType; + this.fieldsToRename = fieldsToRename; + this.fieldToAlter = fieldToAlter; + this.fieldToAlterType = fieldToAlterType; + } + + @NonNull + @Override + public BuildableQuery alterField(@NonNull CqlIdentifier fieldName, @NonNull DataType dataType) { + return new DefaultAlterType( + keyspace, typeName, fieldToAdd, fieldToAddType, fieldsToRename, fieldName, dataType); + } + + @NonNull + @Override + public BuildableQuery addField(@NonNull CqlIdentifier fieldName, @NonNull DataType dataType) { + return new DefaultAlterType( + keyspace, typeName, fieldName, dataType, fieldsToRename, fieldToAlter, fieldToAlterType); + } + + @NonNull + @Override + public AlterTypeRenameFieldEnd renameField( + @NonNull CqlIdentifier from, @NonNull CqlIdentifier to) { + return new DefaultAlterType( + keyspace, + typeName, + fieldToAdd, + fieldToAddType, + ImmutableCollections.append(fieldsToRename, from, to), + fieldToAlter, + fieldToAlterType); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("ALTER TYPE "); + + CqlHelper.qualify(keyspace, typeName, builder); + + if (fieldToAlter != null) { + return builder + .append(" ALTER ") + .append(fieldToAlter.asCql(true)) + .append(" TYPE ") + .append(fieldToAlterType.asCql(true, true)) + .toString(); + } else if (fieldToAdd != null) { + return builder + .append(" ADD ") + .append(fieldToAdd.asCql(true)) + .append(" ") + .append(fieldToAddType.asCql(true, true)) + .toString(); + } else if (!fieldsToRename.isEmpty()) { + builder.append(" RENAME "); + boolean first = true; + for (Map.Entry entry : fieldsToRename.entrySet()) { + if (first) { + first = false; + } else { + builder.append(" AND "); + } + builder + .append(entry.getKey().asCql(true)) + .append(" TO ") + .append(entry.getValue().asCql(true)); + } + return builder.toString(); + } + + // While this is incomplete, we should return partially built query at this point for toString + // purposes. + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getType() { + return typeName; + } + + @Nullable + public CqlIdentifier getFieldToAdd() { + return fieldToAdd; + } + + @Nullable + public DataType getFieldToAddType() { + return fieldToAddType; + } + + @NonNull + public ImmutableMap getFieldsToRename() { + return fieldsToRename; + } + + @Nullable + public CqlIdentifier getFieldToAlter() { + return fieldToAlter; + } + + @Nullable + public DataType getFieldToAlterType() { + return fieldToAlterType; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateAggregate.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateAggregate.java new file mode 100644 index 00000000000..3dea78d82dd --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateAggregate.java @@ -0,0 +1,225 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.schema.CreateAggregateEnd; +import com.datastax.oss.driver.api.querybuilder.schema.CreateAggregateStart; +import com.datastax.oss.driver.api.querybuilder.schema.CreateAggregateStateFunc; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultCreateAggregate + implements CreateAggregateStart, CreateAggregateStateFunc, CreateAggregateEnd { + + private final CqlIdentifier keyspace; + private final CqlIdentifier functionName; + private boolean orReplace; + private boolean ifNotExists; + private final ImmutableList parameters; + private final CqlIdentifier sFunc; + private final DataType sType; + private final CqlIdentifier finalFunc; + private final Term term; + + public DefaultCreateAggregate(@NonNull CqlIdentifier functionName) { + this(null, functionName); + } + + public DefaultCreateAggregate( + @Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier functionName) { + this(keyspace, functionName, false, false, ImmutableList.of(), null, null, null, null); + } + + public DefaultCreateAggregate( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier functionName, + boolean orReplace, + boolean ifNotExists, + @NonNull ImmutableList parameters, + @Nullable CqlIdentifier sFunc, + @Nullable DataType sType, + @Nullable CqlIdentifier finalFunc, + @Nullable Term term) { + this.keyspace = keyspace; + this.functionName = functionName; + this.orReplace = orReplace; + this.ifNotExists = ifNotExists; + this.parameters = parameters; + this.sFunc = sFunc; + this.sType = sType; + this.finalFunc = finalFunc; + this.term = term; + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder(); + + builder.append("CREATE "); + if (orReplace) { + builder.append("OR REPLACE "); + } + builder.append("AGGREGATE "); + + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + CqlHelper.qualify(keyspace, functionName, builder); + + builder.append(" ("); + boolean first = true; + for (DataType param : parameters) { + if (first) { + first = false; + } else { + builder.append(','); + } + builder.append(param.asCql(false, true)); + } + builder.append(')'); + if (sFunc != null) { + builder.append(" SFUNC "); + builder.append(sFunc.asCql(true)); + } + if (sType != null) { + builder.append(" STYPE "); + builder.append(sType.asCql(false, true)); + } + if (finalFunc != null) { + builder.append(" FINALFUNC "); + builder.append(finalFunc.asCql(true)); + } + if (term != null) { + builder.append(" INITCOND "); + term.appendTo(builder); + } + return builder.toString(); + } + + @NonNull + @Override + public CreateAggregateEnd withInitCond(@NonNull Term term) { + return new DefaultCreateAggregate( + keyspace, functionName, orReplace, ifNotExists, parameters, sFunc, sType, finalFunc, term); + } + + @NonNull + @Override + public CreateAggregateStart ifNotExists() { + return new DefaultCreateAggregate( + keyspace, functionName, orReplace, true, parameters, sFunc, sType, finalFunc, term); + } + + @NonNull + @Override + public CreateAggregateStart orReplace() { + return new DefaultCreateAggregate( + keyspace, functionName, true, ifNotExists, parameters, sFunc, sType, finalFunc, term); + } + + @NonNull + @Override + public CreateAggregateStart withParameter(@NonNull DataType paramType) { + return new DefaultCreateAggregate( + keyspace, + functionName, + orReplace, + ifNotExists, + ImmutableCollections.append(parameters, paramType), + sFunc, + sType, + finalFunc, + term); + } + + @NonNull + @Override + public CreateAggregateStateFunc withSFunc(@NonNull CqlIdentifier sFunc) { + return new DefaultCreateAggregate( + keyspace, functionName, orReplace, ifNotExists, parameters, sFunc, sType, finalFunc, term); + } + + @NonNull + @Override + public CreateAggregateEnd withSType(@NonNull DataType sType) { + return new DefaultCreateAggregate( + keyspace, functionName, orReplace, ifNotExists, parameters, sFunc, sType, finalFunc, term); + } + + @NonNull + @Override + public CreateAggregateEnd withFinalFunc(@NonNull CqlIdentifier finalFunc) { + return new DefaultCreateAggregate( + keyspace, functionName, orReplace, ifNotExists, parameters, sFunc, sType, finalFunc, term); + } + + @Override + public String toString() { + return asCql(); + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getFunctionName() { + return functionName; + } + + public boolean isOrReplace() { + return orReplace; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + @NonNull + public ImmutableList getParameters() { + return parameters; + } + + @Nullable + public CqlIdentifier getsFunc() { + return sFunc; + } + + @Nullable + public DataType getsType() { + return sType; + } + + @Nullable + public CqlIdentifier getFinalFunc() { + return finalFunc; + } + + @Nullable + public Term getTerm() { + return term; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateFunction.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateFunction.java new file mode 100644 index 00000000000..77786850ab2 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateFunction.java @@ -0,0 +1,313 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.schema.CreateFunctionEnd; +import com.datastax.oss.driver.api.querybuilder.schema.CreateFunctionStart; +import com.datastax.oss.driver.api.querybuilder.schema.CreateFunctionWithLanguage; +import com.datastax.oss.driver.api.querybuilder.schema.CreateFunctionWithNullOption; +import com.datastax.oss.driver.api.querybuilder.schema.CreateFunctionWithType; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultCreateFunction + implements CreateFunctionStart, + CreateFunctionWithNullOption, + CreateFunctionWithType, + CreateFunctionWithLanguage, + CreateFunctionEnd { + + private final CqlIdentifier keyspace; + private final CqlIdentifier functionName; + private boolean orReplace; + private boolean ifNotExists; + private final ImmutableMap parameters; + private boolean returnsNullOnNull; + private final DataType returnType; + private final String language; + private final String functionBody; + + public DefaultCreateFunction(@NonNull CqlIdentifier functionName) { + this(null, functionName); + } + + public DefaultCreateFunction( + @Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier functionName) { + this(keyspace, functionName, false, false, ImmutableMap.of(), false, null, null, null); + } + + public DefaultCreateFunction( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier functionName, + boolean orReplace, + boolean ifNotExists, + @NonNull ImmutableMap parameters, + boolean returnsNullOnNull, + @Nullable DataType returns, + @Nullable String language, + @Nullable String functionBody) { + this.keyspace = keyspace; + this.functionName = functionName; + this.orReplace = orReplace; + this.ifNotExists = ifNotExists; + this.parameters = parameters; + this.returnsNullOnNull = returnsNullOnNull; + this.returnType = returns; + this.language = language; + this.functionBody = functionBody; + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder(); + + builder.append("CREATE "); + if (orReplace) { + builder.append("OR REPLACE "); + } + builder.append("FUNCTION "); + + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + CqlHelper.qualify(keyspace, functionName, builder); + + builder.append(" ("); + + boolean first = true; + for (Map.Entry param : parameters.entrySet()) { + if (first) { + first = false; + } else { + builder.append(','); + } + builder + .append(param.getKey().asCql(true)) + .append(' ') + .append(param.getValue().asCql(false, true)); + } + builder.append(')'); + if (returnsNullOnNull) { + builder.append(" RETURNS NULL"); + } else { + builder.append(" CALLED"); + } + + builder.append(" ON NULL INPUT"); + + if (returnType == null) { + // return type has not been provided yet. + return builder.toString(); + } + + builder.append(" RETURNS "); + builder.append(returnType.asCql(false, true)); + + if (language == null) { + // language has not been provided yet. + return builder.toString(); + } + + builder.append(" LANGUAGE "); + builder.append(language); + + if (functionBody == null) { + // body has not been provided yet. + return builder.toString(); + } + + builder.append(" AS "); + builder.append(functionBody); + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @NonNull + @Override + public CreateFunctionEnd as(@NonNull String functionBody) { + return new DefaultCreateFunction( + keyspace, + functionName, + orReplace, + ifNotExists, + parameters, + returnsNullOnNull, + returnType, + language, + functionBody); + } + + @NonNull + @Override + public CreateFunctionWithLanguage withLanguage(@NonNull String language) { + return new DefaultCreateFunction( + keyspace, + functionName, + orReplace, + ifNotExists, + parameters, + returnsNullOnNull, + returnType, + language, + functionBody); + } + + @NonNull + @Override + public CreateFunctionWithType returnsType(@NonNull DataType returnType) { + return new DefaultCreateFunction( + keyspace, + functionName, + orReplace, + ifNotExists, + parameters, + returnsNullOnNull, + returnType, + language, + functionBody); + } + + @NonNull + @Override + public CreateFunctionStart ifNotExists() { + return new DefaultCreateFunction( + keyspace, + functionName, + orReplace, + true, + parameters, + returnsNullOnNull, + returnType, + language, + functionBody); + } + + @NonNull + @Override + public CreateFunctionStart orReplace() { + return new DefaultCreateFunction( + keyspace, + functionName, + true, + ifNotExists, + parameters, + returnsNullOnNull, + returnType, + language, + functionBody); + } + + @NonNull + @Override + public CreateFunctionStart withParameter( + @NonNull CqlIdentifier paramName, @NonNull DataType paramType) { + return new DefaultCreateFunction( + keyspace, + functionName, + orReplace, + ifNotExists, + ImmutableCollections.append(parameters, paramName, paramType), + returnsNullOnNull, + returnType, + language, + functionBody); + } + + @NonNull + @Override + public CreateFunctionWithNullOption returnsNullOnNull() { + return new DefaultCreateFunction( + keyspace, + functionName, + orReplace, + ifNotExists, + parameters, + true, + returnType, + language, + functionBody); + } + + @NonNull + @Override + public CreateFunctionWithNullOption calledOnNull() { + return new DefaultCreateFunction( + keyspace, + functionName, + orReplace, + ifNotExists, + parameters, + false, + returnType, + language, + functionBody); + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getFunction() { + return functionName; + } + + public boolean isOrReplace() { + return orReplace; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + @NonNull + public ImmutableMap getParameters() { + return parameters; + } + + public boolean isReturnsNullOnNull() { + return returnsNullOnNull; + } + + @Nullable + public DataType getReturnType() { + return returnType; + } + + @Nullable + public String getLanguage() { + return language; + } + + @Nullable + public String getFunctionBody() { + return functionBody; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateIndex.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateIndex.java new file mode 100644 index 00000000000..c307bbba178 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateIndex.java @@ -0,0 +1,222 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.schema.CreateIndex; +import com.datastax.oss.driver.api.querybuilder.schema.CreateIndexOnTable; +import com.datastax.oss.driver.api.querybuilder.schema.CreateIndexStart; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultCreateIndex implements CreateIndexStart, CreateIndexOnTable, CreateIndex { + + private static final String NO_INDEX_TYPE = "__NO_INDEX_TYPE"; + + private final CqlIdentifier indexName; + + private final boolean ifNotExists; + + private final CqlIdentifier keyspace; + private final CqlIdentifier table; + + private final ImmutableMap columnToIndexType; + + private final String usingClass; + + private final ImmutableMap options; + + public DefaultCreateIndex() { + this(null); + } + + public DefaultCreateIndex(@Nullable CqlIdentifier indexName) { + this(indexName, false, null, null, ImmutableMap.of(), null, ImmutableMap.of()); + } + + public DefaultCreateIndex( + @Nullable CqlIdentifier indexName, + boolean ifNotExists, + @Nullable CqlIdentifier keyspace, + @Nullable CqlIdentifier table, + @NonNull ImmutableMap columnToIndexType, + @Nullable String usingClass, + @NonNull ImmutableMap options) { + this.indexName = indexName; + this.ifNotExists = ifNotExists; + this.keyspace = keyspace; + this.table = table; + this.columnToIndexType = columnToIndexType; + this.usingClass = usingClass; + this.options = options; + } + + @NonNull + @Override + public CreateIndex andColumn(@NonNull CqlIdentifier column, @Nullable String indexType) { + // use placeholder index type when none present as immutable map does not allow null values. + if (indexType == null) { + indexType = NO_INDEX_TYPE; + } + + return new DefaultCreateIndex( + indexName, + ifNotExists, + keyspace, + table, + ImmutableCollections.append(columnToIndexType, column, indexType), + usingClass, + options); + } + + @NonNull + @Override + public CreateIndexStart ifNotExists() { + return new DefaultCreateIndex( + indexName, true, keyspace, table, columnToIndexType, usingClass, options); + } + + @NonNull + @Override + public CreateIndexStart custom(@NonNull String className) { + return new DefaultCreateIndex( + indexName, ifNotExists, keyspace, table, columnToIndexType, className, options); + } + + @NonNull + @Override + public CreateIndexOnTable onTable(CqlIdentifier keyspace, @NonNull CqlIdentifier table) { + return new DefaultCreateIndex( + indexName, ifNotExists, keyspace, table, columnToIndexType, usingClass, options); + } + + @NonNull + @Override + public CreateIndex withOption(@NonNull String name, @NonNull Object value) { + return new DefaultCreateIndex( + indexName, + ifNotExists, + keyspace, + table, + columnToIndexType, + usingClass, + ImmutableCollections.append(options, name, value)); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("CREATE "); + if (usingClass != null) { + builder.append("CUSTOM "); + } + builder.append("INDEX"); + if (ifNotExists) { + builder.append(" IF NOT EXISTS"); + } + + if (indexName != null) { + builder.append(' ').append(indexName.asCql(true)); + } + + if (table == null) { + // Table not provided yet. + return builder.toString(); + } + + builder.append(" ON "); + + CqlHelper.qualify(keyspace, table, builder); + + if (columnToIndexType.isEmpty()) { + // columns not provided yet + return builder.toString(); + } + + builder.append(" ("); + + boolean firstColumn = true; + for (Map.Entry entry : columnToIndexType.entrySet()) { + if (firstColumn) { + firstColumn = false; + } else { + builder.append(","); + } + if (entry.getValue().equals(NO_INDEX_TYPE)) { + builder.append(entry.getKey()); + } else { + builder.append(entry.getValue()).append("(").append(entry.getKey()).append(")"); + } + } + builder.append(")"); + + if (usingClass != null) { + builder.append(" USING '").append(usingClass).append('\''); + } + + if (!options.isEmpty()) { + builder.append(OptionsUtils.buildOptions(options, true)); + } + + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @NonNull + @Override + public Map getOptions() { + return options; + } + + @Nullable + public CqlIdentifier getIndex() { + return indexName; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @Nullable + public CqlIdentifier getTable() { + return table; + } + + @NonNull + public ImmutableMap getColumnToIndexType() { + return columnToIndexType; + } + + @Nullable + public String getUsingClass() { + return usingClass; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateKeyspace.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateKeyspace.java new file mode 100644 index 00000000000..c3908b6e72b --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateKeyspace.java @@ -0,0 +1,100 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.schema.CreateKeyspace; +import com.datastax.oss.driver.api.querybuilder.schema.CreateKeyspaceStart; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultCreateKeyspace implements CreateKeyspace, CreateKeyspaceStart { + + private final CqlIdentifier keyspaceName; + private final boolean ifNotExists; + private final ImmutableMap options; + + public DefaultCreateKeyspace(@NonNull CqlIdentifier keyspaceName) { + this(keyspaceName, false, ImmutableMap.of()); + } + + public DefaultCreateKeyspace( + @NonNull CqlIdentifier keyspaceName, + boolean ifNotExists, + @NonNull ImmutableMap options) { + this.keyspaceName = keyspaceName; + this.ifNotExists = ifNotExists; + this.options = options; + } + + @NonNull + @Override + public CreateKeyspace withOption(@NonNull String name, @NonNull Object value) { + return new DefaultCreateKeyspace( + keyspaceName, ifNotExists, ImmutableCollections.append(options, name, value)); + } + + @NonNull + @Override + public CreateKeyspaceStart ifNotExists() { + return new DefaultCreateKeyspace(keyspaceName, true, options); + } + + @NonNull + @Override + public CreateKeyspace withReplicationOptions(@NonNull Map replicationOptions) { + return withOption("replication", replicationOptions); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder(); + + builder.append("CREATE KEYSPACE "); + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + + builder.append(keyspaceName.asCql(true)); + builder.append(OptionsUtils.buildOptions(options, true)); + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @NonNull + @Override + public Map getOptions() { + return options; + } + + @NonNull + public CqlIdentifier getKeyspace() { + return keyspaceName; + } + + public boolean isIfNotExists() { + return ifNotExists; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateMaterializedView.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateMaterializedView.java new file mode 100644 index 00000000000..710dbfb02df --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateMaterializedView.java @@ -0,0 +1,443 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.schema.CreateMaterializedView; +import com.datastax.oss.driver.api.querybuilder.schema.CreateMaterializedViewPrimaryKey; +import com.datastax.oss.driver.api.querybuilder.schema.CreateMaterializedViewSelection; +import com.datastax.oss.driver.api.querybuilder.schema.CreateMaterializedViewSelectionWithColumns; +import com.datastax.oss.driver.api.querybuilder.schema.CreateMaterializedViewStart; +import com.datastax.oss.driver.api.querybuilder.schema.CreateMaterializedViewWhere; +import com.datastax.oss.driver.api.querybuilder.schema.CreateMaterializedViewWhereStart; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultCreateMaterializedView + implements CreateMaterializedViewStart, + CreateMaterializedViewSelectionWithColumns, + CreateMaterializedViewWhere, + CreateMaterializedViewPrimaryKey, + CreateMaterializedView { + + private final CqlIdentifier keyspace; + private final CqlIdentifier viewName; + + private final boolean ifNotExists; + + private final CqlIdentifier baseTableKeyspace; + private final CqlIdentifier baseTable; + + private final ImmutableList selectors; + private final ImmutableList whereRelations; + private final ImmutableSet partitionKeyColumns; + private final ImmutableSet clusteringKeyColumns; + + private final ImmutableMap orderings; + + private final ImmutableMap options; + + public DefaultCreateMaterializedView(@NonNull CqlIdentifier viewName) { + this(null, viewName); + } + + public DefaultCreateMaterializedView( + @Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier viewName) { + this( + keyspace, + viewName, + false, + null, + null, + ImmutableList.of(), + ImmutableList.of(), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableMap.of(), + ImmutableMap.of()); + } + + public DefaultCreateMaterializedView( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier viewName, + boolean ifNotExists, + @Nullable CqlIdentifier baseTableKeyspace, + @Nullable CqlIdentifier baseTable, + @NonNull ImmutableList selectors, + @NonNull ImmutableList whereRelations, + @NonNull ImmutableSet partitionKeyColumns, + @NonNull ImmutableSet clusteringKeyColumns, + @NonNull ImmutableMap orderings, + @NonNull ImmutableMap options) { + this.keyspace = keyspace; + this.viewName = viewName; + this.ifNotExists = ifNotExists; + this.baseTableKeyspace = baseTableKeyspace; + this.baseTable = baseTable; + this.selectors = selectors; + this.whereRelations = whereRelations; + this.partitionKeyColumns = partitionKeyColumns; + this.clusteringKeyColumns = clusteringKeyColumns; + this.orderings = orderings; + this.options = options; + } + + @NonNull + @Override + public CreateMaterializedViewWhereStart all() { + return new DefaultCreateMaterializedView( + keyspace, + viewName, + ifNotExists, + baseTableKeyspace, + baseTable, + ImmutableCollections.append(selectors, Selector.all()), + whereRelations, + partitionKeyColumns, + clusteringKeyColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateMaterializedViewSelectionWithColumns column(@NonNull CqlIdentifier columnName) { + return new DefaultCreateMaterializedView( + keyspace, + viewName, + ifNotExists, + baseTableKeyspace, + baseTable, + ImmutableCollections.append(selectors, Selector.column(columnName)), + whereRelations, + partitionKeyColumns, + clusteringKeyColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateMaterializedViewSelectionWithColumns columnsIds( + @NonNull Iterable columnIds) { + ImmutableList.Builder columnSelectors = ImmutableList.builder(); + for (CqlIdentifier column : columnIds) { + columnSelectors.add(Selector.column(column)); + } + return new DefaultCreateMaterializedView( + keyspace, + viewName, + ifNotExists, + baseTableKeyspace, + baseTable, + ImmutableCollections.concat(selectors, columnSelectors.build()), + whereRelations, + partitionKeyColumns, + clusteringKeyColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateMaterializedViewWhere where(@NonNull Relation relation) { + return new DefaultCreateMaterializedView( + keyspace, + viewName, + ifNotExists, + baseTableKeyspace, + baseTable, + selectors, + ImmutableCollections.append(whereRelations, relation), + partitionKeyColumns, + clusteringKeyColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateMaterializedViewWhere where(@NonNull Iterable additionalRelations) { + return new DefaultCreateMaterializedView( + keyspace, + viewName, + ifNotExists, + baseTableKeyspace, + baseTable, + selectors, + ImmutableCollections.concat(whereRelations, additionalRelations), + partitionKeyColumns, + clusteringKeyColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateMaterializedViewPrimaryKey withPartitionKey(@NonNull CqlIdentifier columnName) { + return new DefaultCreateMaterializedView( + keyspace, + viewName, + ifNotExists, + baseTableKeyspace, + baseTable, + selectors, + whereRelations, + Utils.appendSet(partitionKeyColumns, columnName), + clusteringKeyColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateMaterializedViewPrimaryKey withClusteringColumn(@NonNull CqlIdentifier columnName) { + return new DefaultCreateMaterializedView( + keyspace, + viewName, + ifNotExists, + baseTableKeyspace, + baseTable, + selectors, + whereRelations, + partitionKeyColumns, + Utils.appendSet(clusteringKeyColumns, columnName), + orderings, + options); + } + + @NonNull + @Override + public CreateMaterializedViewStart ifNotExists() { + return new DefaultCreateMaterializedView( + keyspace, + viewName, + true, + baseTableKeyspace, + baseTable, + selectors, + whereRelations, + partitionKeyColumns, + clusteringKeyColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateMaterializedViewSelection asSelectFrom(@NonNull CqlIdentifier table) { + return asSelectFrom(null, table); + } + + @NonNull + @Override + public CreateMaterializedViewSelection asSelectFrom( + CqlIdentifier baseTableKeyspace, @NonNull CqlIdentifier baseTable) { + return new DefaultCreateMaterializedView( + keyspace, + viewName, + ifNotExists, + baseTableKeyspace, + baseTable, + selectors, + whereRelations, + partitionKeyColumns, + clusteringKeyColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateMaterializedView withClusteringOrderByIds( + @NonNull Map orderings) { + return withClusteringOrders(ImmutableCollections.concat(this.orderings, orderings)); + } + + @NonNull + @Override + public CreateMaterializedView withClusteringOrder( + @NonNull CqlIdentifier columnName, @NonNull ClusteringOrder order) { + return withClusteringOrders(ImmutableCollections.append(orderings, columnName, order)); + } + + @NonNull + public CreateMaterializedView withClusteringOrders( + @NonNull ImmutableMap orderings) { + return new DefaultCreateMaterializedView( + keyspace, + viewName, + ifNotExists, + baseTableKeyspace, + baseTable, + selectors, + whereRelations, + partitionKeyColumns, + clusteringKeyColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateMaterializedView withOption(@NonNull String name, @NonNull Object value) { + return new DefaultCreateMaterializedView( + keyspace, + viewName, + ifNotExists, + baseTableKeyspace, + baseTable, + selectors, + whereRelations, + partitionKeyColumns, + clusteringKeyColumns, + orderings, + ImmutableCollections.append(options, name, value)); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("CREATE MATERIALIZED VIEW "); + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + + CqlHelper.qualify(keyspace, viewName, builder); + + if (selectors.isEmpty()) { + // selectors not provided yet. + return builder.toString(); + } + + CqlHelper.append(selectors, builder, " AS SELECT ", ",", " FROM "); + + if (baseTable == null) { + // base table not provided yet. + return builder.toString(); + } + + CqlHelper.qualify(baseTableKeyspace, baseTable, builder); + + if (whereRelations.isEmpty()) { + // where clause not provided yet. + return builder.toString(); + } + + CqlHelper.append(whereRelations, builder, " WHERE ", " AND ", " "); + + CqlHelper.buildPrimaryKey(partitionKeyColumns, clusteringKeyColumns, builder); + + if (!orderings.isEmpty() || !options.isEmpty()) { + boolean firstOption = true; + + if (!orderings.isEmpty()) { + builder.append(" WITH "); + firstOption = false; + builder.append("CLUSTERING ORDER BY ("); + boolean firstClustering = true; + + for (Map.Entry ordering : orderings.entrySet()) { + if (firstClustering) { + firstClustering = false; + } else { + builder.append(','); + } + builder + .append(ordering.getKey().asCql(true)) + .append(' ') + .append(ordering.getValue().toString()); + } + + builder.append(')'); + } + + builder.append(OptionsUtils.buildOptions(options, firstOption)); + } + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @NonNull + @Override + public Map getOptions() { + return options; + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getMaterializedView() { + return viewName; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + @Nullable + public CqlIdentifier getBaseTableKeyspace() { + return baseTableKeyspace; + } + + @Nullable + public CqlIdentifier getBaseTable() { + return baseTable; + } + + @NonNull + public ImmutableList getSelectors() { + return selectors; + } + + @NonNull + public ImmutableList getWhereRelations() { + return whereRelations; + } + + @NonNull + public ImmutableSet getPartitionKeyColumns() { + return partitionKeyColumns; + } + + @NonNull + public ImmutableSet getClusteringKeyColumns() { + return clusteringKeyColumns; + } + + @NonNull + public ImmutableMap getOrderings() { + return orderings; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateTable.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateTable.java new file mode 100644 index 00000000000..1de5651c2ec --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateTable.java @@ -0,0 +1,396 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import static com.datastax.oss.driver.internal.querybuilder.schema.Utils.appendSet; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.schema.CreateTable; +import com.datastax.oss.driver.api.querybuilder.schema.CreateTableStart; +import com.datastax.oss.driver.api.querybuilder.schema.CreateTableWithOptions; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultCreateTable implements CreateTableStart, CreateTable, CreateTableWithOptions { + + private final CqlIdentifier keyspace; + private final CqlIdentifier tableName; + + private final boolean ifNotExists; + private final boolean compactStorage; + + private final ImmutableMap options; + + private final ImmutableMap columnsInOrder; + + private final ImmutableSet partitionKeyColumns; + private final ImmutableSet clusteringKeyColumns; + private final ImmutableSet staticColumns; + private final ImmutableSet regularColumns; + + private final ImmutableMap orderings; + + public DefaultCreateTable(@NonNull CqlIdentifier tableName) { + this(null, tableName); + } + + public DefaultCreateTable(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier tableName) { + this( + keyspace, + tableName, + false, + false, + ImmutableMap.of(), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableMap.of(), + ImmutableMap.of()); + } + + public DefaultCreateTable( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier tableName, + boolean ifNotExists, + boolean compactStorage, + @NonNull ImmutableMap columnsInOrder, + @NonNull ImmutableSet partitionKeyColumns, + @NonNull ImmutableSet clusteringKeyColumns, + @NonNull ImmutableSet staticColumns, + @NonNull ImmutableSet regularColumns, + @NonNull ImmutableMap orderings, + @NonNull ImmutableMap options) { + this.keyspace = keyspace; + this.tableName = tableName; + this.ifNotExists = ifNotExists; + this.compactStorage = compactStorage; + this.columnsInOrder = columnsInOrder; + this.partitionKeyColumns = partitionKeyColumns; + this.clusteringKeyColumns = clusteringKeyColumns; + this.staticColumns = staticColumns; + this.regularColumns = regularColumns; + this.orderings = orderings; + this.options = options; + } + + @NonNull + @Override + public CreateTableStart ifNotExists() { + return new DefaultCreateTable( + keyspace, + tableName, + true, + compactStorage, + columnsInOrder, + partitionKeyColumns, + clusteringKeyColumns, + staticColumns, + regularColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateTable withPartitionKey( + @NonNull CqlIdentifier columnName, @NonNull DataType dataType) { + return new DefaultCreateTable( + keyspace, + tableName, + ifNotExists, + compactStorage, + ImmutableCollections.append(columnsInOrder, columnName, dataType), + appendSet(partitionKeyColumns, columnName), + clusteringKeyColumns, + staticColumns, + regularColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateTable withClusteringColumn( + @NonNull CqlIdentifier columnName, @NonNull DataType dataType) { + return new DefaultCreateTable( + keyspace, + tableName, + ifNotExists, + compactStorage, + ImmutableCollections.append(columnsInOrder, columnName, dataType), + partitionKeyColumns, + appendSet(clusteringKeyColumns, columnName), + staticColumns, + regularColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateTable withColumn(@NonNull CqlIdentifier columnName, @NonNull DataType dataType) { + return new DefaultCreateTable( + keyspace, + tableName, + ifNotExists, + compactStorage, + ImmutableCollections.append(columnsInOrder, columnName, dataType), + partitionKeyColumns, + clusteringKeyColumns, + staticColumns, + appendSet(regularColumns, columnName), + orderings, + options); + } + + @NonNull + @Override + public CreateTable withStaticColumn( + @NonNull CqlIdentifier columnName, @NonNull DataType dataType) { + return new DefaultCreateTable( + keyspace, + tableName, + ifNotExists, + compactStorage, + ImmutableCollections.append(columnsInOrder, columnName, dataType), + partitionKeyColumns, + clusteringKeyColumns, + appendSet(staticColumns, columnName), + regularColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateTableWithOptions withCompactStorage() { + return new DefaultCreateTable( + keyspace, + tableName, + ifNotExists, + true, + columnsInOrder, + partitionKeyColumns, + clusteringKeyColumns, + staticColumns, + regularColumns, + orderings, + options); + } + + @NonNull + @Override + public CreateTableWithOptions withClusteringOrderByIds( + @NonNull Map orderings) { + return withClusteringOrders(ImmutableCollections.concat(this.orderings, orderings)); + } + + @NonNull + @Override + public CreateTableWithOptions withClusteringOrder( + @NonNull CqlIdentifier columnName, @NonNull ClusteringOrder order) { + return withClusteringOrders(ImmutableCollections.append(orderings, columnName, order)); + } + + @NonNull + public CreateTableWithOptions withClusteringOrders( + @NonNull ImmutableMap orderings) { + return new DefaultCreateTable( + keyspace, + tableName, + ifNotExists, + compactStorage, + columnsInOrder, + partitionKeyColumns, + clusteringKeyColumns, + staticColumns, + regularColumns, + orderings, + options); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder(); + + builder.append("CREATE TABLE "); + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + + CqlHelper.qualify(keyspace, tableName, builder); + + if (columnsInOrder.isEmpty()) { + // no columns provided yet. + return builder.toString(); + } + + boolean singlePrimaryKey = partitionKeyColumns.size() == 1 && clusteringKeyColumns.size() == 0; + + builder.append(" ("); + + boolean first = true; + for (Map.Entry column : columnsInOrder.entrySet()) { + if (first) { + first = false; + } else { + builder.append(','); + } + builder + .append(column.getKey().asCql(true)) + .append(' ') + .append(column.getValue().asCql(true, true)); + + if (singlePrimaryKey && partitionKeyColumns.contains(column.getKey())) { + builder.append(" PRIMARY KEY"); + } else if (staticColumns.contains(column.getKey())) { + builder.append(" STATIC"); + } + } + + if (!singlePrimaryKey) { + builder.append(","); + CqlHelper.buildPrimaryKey(partitionKeyColumns, clusteringKeyColumns, builder); + } + + builder.append(')'); + + if (compactStorage || !orderings.isEmpty() || !options.isEmpty()) { + boolean firstOption = true; + + if (compactStorage) { + firstOption = false; + builder.append(" WITH COMPACT STORAGE"); + } + + if (!orderings.isEmpty()) { + if (firstOption) { + builder.append(" WITH "); + firstOption = false; + } else { + builder.append(" AND "); + } + builder.append("CLUSTERING ORDER BY ("); + boolean firstClustering = true; + + for (Map.Entry ordering : orderings.entrySet()) { + if (firstClustering) { + firstClustering = false; + } else { + builder.append(','); + } + builder + .append(ordering.getKey().asCql(true)) + .append(' ') + .append(ordering.getValue().toString()); + } + + builder.append(')'); + } + + builder.append(OptionsUtils.buildOptions(options, firstOption)); + } + + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @NonNull + @Override + public CreateTable withOption(@NonNull String name, @NonNull Object value) { + return new DefaultCreateTable( + keyspace, + tableName, + ifNotExists, + compactStorage, + columnsInOrder, + partitionKeyColumns, + clusteringKeyColumns, + staticColumns, + regularColumns, + orderings, + ImmutableCollections.append(options, name, value)); + } + + @NonNull + @Override + public Map getOptions() { + return options; + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getTable() { + return tableName; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + public boolean isCompactStorage() { + return compactStorage; + } + + @NonNull + public ImmutableMap getColumnsInOrder() { + return columnsInOrder; + } + + @NonNull + public ImmutableSet getPartitionKeyColumns() { + return partitionKeyColumns; + } + + @NonNull + public ImmutableSet getClusteringKeyColumns() { + return clusteringKeyColumns; + } + + @NonNull + public ImmutableSet getStaticColumns() { + return staticColumns; + } + + @NonNull + public ImmutableSet getRegularColumns() { + return regularColumns; + } + + @NonNull + public ImmutableMap getOrderings() { + return orderings; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateType.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateType.java new file mode 100644 index 00000000000..de5d1841bfe --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultCreateType.java @@ -0,0 +1,131 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.schema.CreateType; +import com.datastax.oss.driver.api.querybuilder.schema.CreateTypeStart; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultCreateType implements CreateTypeStart, CreateType { + + private final CqlIdentifier keyspace; + private final CqlIdentifier typeName; + private final boolean ifNotExists; + private final ImmutableMap fieldsInOrder; + + public DefaultCreateType(@NonNull CqlIdentifier typeName) { + this(null, typeName); + } + + public DefaultCreateType(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier typeName) { + this(keyspace, typeName, false, ImmutableMap.of()); + } + + public DefaultCreateType( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier typeName, + boolean ifNotExists, + @NonNull ImmutableMap fieldsInOrder) { + this.keyspace = keyspace; + this.typeName = typeName; + this.ifNotExists = ifNotExists; + this.fieldsInOrder = fieldsInOrder; + } + + @NonNull + @Override + public CreateType withField(@NonNull CqlIdentifier fieldName, @NonNull DataType dataType) { + return new DefaultCreateType( + keyspace, + typeName, + ifNotExists, + ImmutableCollections.append(fieldsInOrder, fieldName, dataType)); + } + + @NonNull + @Override + public CreateTypeStart ifNotExists() { + return new DefaultCreateType(keyspace, typeName, true, fieldsInOrder); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder(); + + builder.append("CREATE TYPE "); + if (ifNotExists) { + builder.append("IF NOT EXISTS "); + } + + CqlHelper.qualify(keyspace, typeName, builder); + + if (fieldsInOrder.isEmpty()) { + // no fields provided yet. + return builder.toString(); + } + + builder.append(" ("); + + boolean first = true; + for (Map.Entry field : fieldsInOrder.entrySet()) { + if (first) { + first = false; + } else { + builder.append(','); + } + builder + .append(field.getKey().asCql(true)) + .append(' ') + .append(field.getValue().asCql(true, true)); + } + builder.append(')'); + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getType() { + return typeName; + } + + public boolean isIfNotExists() { + return ifNotExists; + } + + @NonNull + public ImmutableMap getFieldsInOrder() { + return fieldsInOrder; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultDrop.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultDrop.java new file mode 100644 index 00000000000..49a692a1f84 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultDrop.java @@ -0,0 +1,99 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.schema.Drop; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultDrop implements Drop { + + private final CqlIdentifier keyspace; + private final CqlIdentifier itemName; + private final String schemaTypeName; + + private final boolean ifExists; + + public DefaultDrop(@NonNull CqlIdentifier itemName, @NonNull String schemaTypeName) { + this(null, itemName, schemaTypeName); + } + + public DefaultDrop( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier itemName, + @NonNull String schemaTypeName) { + this(keyspace, itemName, schemaTypeName, false); + } + + public DefaultDrop( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier itemName, + @NonNull String schemaTypeName, + boolean ifExists) { + this.keyspace = keyspace; + this.itemName = itemName; + this.schemaTypeName = schemaTypeName; + this.ifExists = ifExists; + } + + @NonNull + @Override + public Drop ifExists() { + return new DefaultDrop(keyspace, itemName, schemaTypeName, true); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("DROP ").append(schemaTypeName).append(' '); + + if (ifExists) { + builder.append("IF EXISTS "); + } + + CqlHelper.qualify(keyspace, itemName, builder); + + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getName() { + return itemName; + } + + @NonNull + public String getSchemaType() { + return schemaTypeName; + } + + public boolean isIfExists() { + return ifExists; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultDropKeyspace.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultDropKeyspace.java new file mode 100644 index 00000000000..2ded8b95e20 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/DefaultDropKeyspace.java @@ -0,0 +1,71 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.schema.Drop; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultDropKeyspace implements Drop { + + private final CqlIdentifier keyspaceName; + private final boolean ifExists; + + public DefaultDropKeyspace(@NonNull CqlIdentifier keyspaceName) { + this(keyspaceName, false); + } + + public DefaultDropKeyspace(@NonNull CqlIdentifier keyspaceName, boolean ifExists) { + this.keyspaceName = keyspaceName; + this.ifExists = ifExists; + } + + @NonNull + @Override + public Drop ifExists() { + return new DefaultDropKeyspace(keyspaceName, true); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("DROP KEYSPACE "); + + if (ifExists) { + builder.append("IF EXISTS "); + } + + builder.append(keyspaceName.asCql(true)); + + return builder.toString(); + } + + @Override + public String toString() { + return asCql(); + } + + @NonNull + public CqlIdentifier getKeyspace() { + return keyspaceName; + } + + public boolean isIfExists() { + return ifExists; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/OptionsUtils.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/OptionsUtils.java new file mode 100644 index 00000000000..83ff28503ae --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/OptionsUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; + +public class OptionsUtils { + @NonNull + public static String buildOptions(@NonNull Map options, boolean first) { + StringBuilder builder = new StringBuilder(); + for (Map.Entry option : options.entrySet()) { + if (first) { + builder.append(" WITH "); + first = false; + } else { + builder.append(" AND "); + } + String value = OptionsUtils.extractOptionValue(option.getValue()); + builder.append(option.getKey()).append("=").append(value); + } + return builder.toString(); + } + + @NonNull + private static String extractOptionValue(@NonNull Object option) { + StringBuilder optionValue = new StringBuilder(); + if (option instanceof String) { + optionValue.append("'").append((String) option).append("'"); + } else if (option instanceof Map) { + @SuppressWarnings("unchecked") + Map optionMap = (Map) option; + boolean first = true; + optionValue.append("{"); + for (Map.Entry subOption : optionMap.entrySet()) { + if (first) { + first = false; + } else { + optionValue.append(","); + } + optionValue + .append("'") + .append(subOption.getKey()) + .append("':") + .append(extractOptionValue(subOption.getValue())); + } + optionValue.append("}"); + } else { + optionValue.append(option); + } + return optionValue.toString(); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/Utils.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/Utils.java new file mode 100644 index 00000000000..2c8ccdd6e6a --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/Utils.java @@ -0,0 +1,34 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import edu.umd.cs.findbugs.annotations.NonNull; + +public class Utils { + /** Convenience method for creating a new {@link ImmutableSet} with an appended value. */ + @NonNull + public static ImmutableSet appendSet(@NonNull ImmutableSet set, @NonNull E newValue) { + return ImmutableSet.builder().addAll(set).add(newValue).build(); + } + + /** Convenience method for creating a new {@link ImmutableSet} with concatenated iterable. */ + @NonNull + public static ImmutableSet concatSet( + @NonNull ImmutableSet set, @NonNull Iterable toConcat) { + return ImmutableSet.builder().addAll(set).addAll(toConcat).build(); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultCompactionStrategy.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultCompactionStrategy.java new file mode 100644 index 00000000000..42b93c27d50 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultCompactionStrategy.java @@ -0,0 +1,48 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema.compaction; + +import com.datastax.oss.driver.api.querybuilder.schema.compaction.CompactionStrategy; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public abstract class DefaultCompactionStrategy> + implements CompactionStrategy { + + private final ImmutableMap options; + + protected DefaultCompactionStrategy(@NonNull String className) { + this(ImmutableMap.of("class", className)); + } + + protected DefaultCompactionStrategy(@NonNull ImmutableMap options) { + this.options = options; + } + + @NonNull + public ImmutableMap getInternalOptions() { + return options; + } + + @NonNull + @Override + public Map getOptions() { + return options; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultLeveledCompactionStrategy.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultLeveledCompactionStrategy.java new file mode 100644 index 00000000000..d0ddd4a7420 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultLeveledCompactionStrategy.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema.compaction; + +import com.datastax.oss.driver.api.querybuilder.schema.compaction.LeveledCompactionStrategy; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultLeveledCompactionStrategy + extends DefaultCompactionStrategy + implements LeveledCompactionStrategy { + + public DefaultLeveledCompactionStrategy() { + super("LeveledCompactionStrategy"); + } + + protected DefaultLeveledCompactionStrategy(@NonNull ImmutableMap options) { + super(options); + } + + @NonNull + @Override + public DefaultLeveledCompactionStrategy withOption(@NonNull String name, @NonNull Object value) { + return new DefaultLeveledCompactionStrategy( + ImmutableCollections.append(getInternalOptions(), name, value)); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultSizeTieredCompactionStrategy.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultSizeTieredCompactionStrategy.java new file mode 100644 index 00000000000..91c339fd8a4 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultSizeTieredCompactionStrategy.java @@ -0,0 +1,44 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema.compaction; + +import com.datastax.oss.driver.api.querybuilder.schema.compaction.SizeTieredCompactionStrategy; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultSizeTieredCompactionStrategy + extends DefaultCompactionStrategy + implements SizeTieredCompactionStrategy { + + public DefaultSizeTieredCompactionStrategy() { + super("SizeTieredCompactionStrategy"); + } + + protected DefaultSizeTieredCompactionStrategy(@NonNull ImmutableMap options) { + super(options); + } + + @NonNull + @Override + public DefaultSizeTieredCompactionStrategy withOption( + @NonNull String name, @NonNull Object value) { + return new DefaultSizeTieredCompactionStrategy( + ImmutableCollections.append(getInternalOptions(), name, value)); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultTimeWindowCompactionStrategy.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultTimeWindowCompactionStrategy.java new file mode 100644 index 00000000000..e69ca8c93a3 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/schema/compaction/DefaultTimeWindowCompactionStrategy.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.schema.compaction; + +import com.datastax.oss.driver.api.querybuilder.schema.compaction.TimeWindowCompactionStrategy; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultTimeWindowCompactionStrategy + extends DefaultCompactionStrategy + implements TimeWindowCompactionStrategy { + public DefaultTimeWindowCompactionStrategy() { + super("TimeWindowCompactionStrategy"); + } + + protected DefaultTimeWindowCompactionStrategy(@NonNull ImmutableMap options) { + super(options); + } + + @NonNull + @Override + public DefaultTimeWindowCompactionStrategy withOption( + @NonNull String name, @NonNull Object value) { + return new DefaultTimeWindowCompactionStrategy( + ImmutableCollections.append(getInternalOptions(), name, value)); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/AllSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/AllSelector.java new file mode 100644 index 00000000000..ed0c2977420 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/AllSelector.java @@ -0,0 +1,42 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; + +public enum AllSelector implements Selector { + INSTANCE; + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + throw new IllegalStateException("Can't alias the '*' selector"); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append('*'); + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return null; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ArithmeticSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ArithmeticSelector.java new file mode 100644 index 00000000000..61998499cd5 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ArithmeticSelector.java @@ -0,0 +1,52 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.internal.querybuilder.ArithmeticOperator; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public abstract class ArithmeticSelector implements Selector { + + protected final ArithmeticOperator operator; + + protected ArithmeticSelector(@NonNull ArithmeticOperator operator) { + Preconditions.checkNotNull(operator); + this.operator = operator; + } + + @NonNull + public ArithmeticOperator getOperator() { + return operator; + } + + protected static void appendAndMaybeParenthesize( + int myPrecedence, @NonNull Selector child, @NonNull StringBuilder builder) { + boolean parenthesize = + (child instanceof ArithmeticSelector) + && (((ArithmeticSelector) child).operator.getPrecedenceLeft() < myPrecedence); + if (parenthesize) { + builder.append('('); + } + child.appendTo(builder); + if (parenthesize) { + builder.append(')'); + } + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/BinaryArithmeticSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/BinaryArithmeticSelector.java new file mode 100644 index 00000000000..c1cdf8cd766 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/BinaryArithmeticSelector.java @@ -0,0 +1,103 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.internal.querybuilder.ArithmeticOperator; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class BinaryArithmeticSelector extends ArithmeticSelector { + + private final Selector left; + private final Selector right; + private final CqlIdentifier alias; + + public BinaryArithmeticSelector( + @NonNull ArithmeticOperator operator, @NonNull Selector left, @NonNull Selector right) { + this(operator, left, right, null); + } + + public BinaryArithmeticSelector( + @NonNull ArithmeticOperator operator, + @NonNull Selector left, + @NonNull Selector right, + @Nullable CqlIdentifier alias) { + super(operator); + Preconditions.checkNotNull(left); + Preconditions.checkNotNull(right); + this.left = left; + this.right = right; + this.alias = alias; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new BinaryArithmeticSelector(operator, left, right, alias); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + appendAndMaybeParenthesize(operator.getPrecedenceLeft(), left, builder); + builder.append(operator.getSymbol()); + appendAndMaybeParenthesize(operator.getPrecedenceRight(), right, builder); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @NonNull + public Selector getLeft() { + return left; + } + + @NonNull + public Selector getRight() { + return right; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof BinaryArithmeticSelector) { + BinaryArithmeticSelector that = (BinaryArithmeticSelector) other; + return this.operator.equals(that.operator) + && this.left.equals(that.left) + && this.right.equals(that.right) + && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(operator, left, right, alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/CastSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/CastSelector.java new file mode 100644 index 00000000000..b397cdcaf16 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/CastSelector.java @@ -0,0 +1,98 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class CastSelector implements Selector { + + private final Selector selector; + private final DataType targetType; + private final CqlIdentifier alias; + + public CastSelector(@NonNull Selector selector, @NonNull DataType targetType) { + this(selector, targetType, null); + } + + public CastSelector( + @NonNull Selector selector, @NonNull DataType targetType, @Nullable CqlIdentifier alias) { + Preconditions.checkNotNull(selector); + Preconditions.checkNotNull(targetType); + Preconditions.checkArgument(selector.getAlias() == null, "Inner selector can't be aliased"); + this.selector = selector; + this.targetType = targetType; + this.alias = alias; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append("CAST("); + selector.appendTo(builder); + builder.append(" AS ").append(targetType.asCql(false, true)).append(')'); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new CastSelector(selector, targetType, alias); + } + + @NonNull + public Selector getSelector() { + return selector; + } + + @NonNull + public DataType getTargetType() { + return targetType; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof CastSelector) { + CastSelector that = (CastSelector) other; + return this.selector.equals(that.selector) + && this.targetType.equals(that.targetType) + && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(selector, targetType, alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/CollectionSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/CollectionSelector.java new file mode 100644 index 00000000000..ccec72cebee --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/CollectionSelector.java @@ -0,0 +1,109 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.Iterables; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public abstract class CollectionSelector implements Selector { + + private final Iterable elementSelectors; + private final String opening; + private final String closing; + private final CqlIdentifier alias; + + protected CollectionSelector( + @NonNull Iterable elementSelectors, + @NonNull String opening, + @NonNull String closing, + @Nullable CqlIdentifier alias) { + Preconditions.checkNotNull(elementSelectors); + Preconditions.checkArgument( + elementSelectors.iterator().hasNext(), "Must have at least one selector"); + checkNoAlias(elementSelectors); + Preconditions.checkNotNull(opening); + Preconditions.checkNotNull(closing); + this.elementSelectors = elementSelectors; + this.opening = opening; + this.closing = closing; + this.alias = alias; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + CqlHelper.append(elementSelectors, builder, opening, ",", closing); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @NonNull + public Iterable getElementSelectors() { + return elementSelectors; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof CollectionSelector) { + CollectionSelector that = (CollectionSelector) other; + return Iterables.elementsEqual(this.elementSelectors, that.elementSelectors) + && this.opening.equals(that.opening) + && this.closing.equals(that.closing) + && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(elementSelectors, opening, closing, alias); + } + + private static void checkNoAlias(Iterable elementSelectors) { + String offendingAliases = null; + for (Selector selector : elementSelectors) { + CqlIdentifier alias = selector.getAlias(); + if (alias != null) { + if (offendingAliases == null) { + offendingAliases = alias.asCql(true); + } else { + offendingAliases += ", " + alias.asCql(true); + } + } + } + if (offendingAliases != null) { + throw new IllegalArgumentException( + "Can't use aliases in selection list, offending aliases: " + offendingAliases); + } + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ColumnSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ColumnSelector.java new file mode 100644 index 00000000000..6404bcc15e7 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ColumnSelector.java @@ -0,0 +1,83 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class ColumnSelector implements Selector { + + private final CqlIdentifier columnId; + private final CqlIdentifier alias; + + public ColumnSelector(@NonNull CqlIdentifier columnId) { + this(columnId, null); + } + + public ColumnSelector(@NonNull CqlIdentifier columnId, @Nullable CqlIdentifier alias) { + Preconditions.checkNotNull(columnId); + this.columnId = columnId; + this.alias = alias; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new ColumnSelector(columnId, alias); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append(columnId.asCql(true)); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @NonNull + public CqlIdentifier getColumnId() { + return columnId; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof ColumnSelector) { + ColumnSelector that = (ColumnSelector) other; + return this.columnId.equals(that.columnId) && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(columnId, alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/CountAllSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/CountAllSelector.java new file mode 100644 index 00000000000..07357e7c773 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/CountAllSelector.java @@ -0,0 +1,74 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class CountAllSelector implements Selector { + + private final CqlIdentifier alias; + + public CountAllSelector() { + this(null); + } + + public CountAllSelector(@Nullable CqlIdentifier alias) { + this.alias = alias; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new CountAllSelector(alias); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append("count(*)"); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof CountAllSelector) { + CountAllSelector that = (CountAllSelector) other; + return Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return (alias == null) ? 0 : alias.hashCode(); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/DefaultBindMarker.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/DefaultBindMarker.java new file mode 100644 index 00000000000..231a812a98f --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/DefaultBindMarker.java @@ -0,0 +1,55 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.BindMarker; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultBindMarker implements BindMarker { + + private final CqlIdentifier id; + + public DefaultBindMarker(@Nullable CqlIdentifier id) { + this.id = id; + } + + public DefaultBindMarker() { + this(null); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + if (id == null) { + builder.append('?'); + } else { + builder.append(':').append(id.asCql(true)); + } + } + + @Override + public boolean isIdempotent() { + return true; + } + + @Nullable + public CqlIdentifier getId() { + return id; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/DefaultSelect.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/DefaultSelect.java new file mode 100644 index 00000000000..6ab7c8a4065 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/DefaultSelect.java @@ -0,0 +1,513 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder; +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import com.datastax.oss.driver.api.querybuilder.BindMarker; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.select.Select; +import com.datastax.oss.driver.api.querybuilder.select.SelectFrom; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultSelect implements SelectFrom, Select { + + private static final ImmutableList SELECT_ALL = ImmutableList.of(AllSelector.INSTANCE); + + private final CqlIdentifier keyspace; + private final CqlIdentifier table; + private final boolean isJson; + private final boolean isDistinct; + private final ImmutableList selectors; + private final ImmutableList relations; + private final ImmutableList groupByClauses; + private final ImmutableMap orderings; + private final Object limit; + private final Object perPartitionLimit; + private final boolean allowsFiltering; + + public DefaultSelect(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) { + this( + keyspace, + table, + false, + false, + ImmutableList.of(), + ImmutableList.of(), + ImmutableList.of(), + ImmutableMap.of(), + null, + null, + false); + } + + /** + * This constructor is public only as a convenience for custom extensions of the query builder. + * + * @param selectors if it contains {@link AllSelector#INSTANCE}, that must be the only element. + * This isn't re-checked because methods that call this constructor internally already do it, + * make sure you do it yourself. + */ + public DefaultSelect( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier table, + boolean isJson, + boolean isDistinct, + @NonNull ImmutableList selectors, + @NonNull ImmutableList relations, + @NonNull ImmutableList groupByClauses, + @NonNull ImmutableMap orderings, + @Nullable Object limit, + @Nullable Object perPartitionLimit, + boolean allowsFiltering) { + this.groupByClauses = groupByClauses; + this.orderings = orderings; + Preconditions.checkArgument( + limit == null + || (limit instanceof Integer && (Integer) limit > 0) + || limit instanceof BindMarker, + "limit must be a strictly positive integer or a bind marker"); + this.keyspace = keyspace; + this.table = table; + this.isJson = isJson; + this.isDistinct = isDistinct; + this.selectors = selectors; + this.relations = relations; + this.limit = limit; + this.perPartitionLimit = perPartitionLimit; + this.allowsFiltering = allowsFiltering; + } + + @NonNull + @Override + public SelectFrom json() { + return new DefaultSelect( + keyspace, + table, + true, + isDistinct, + selectors, + relations, + groupByClauses, + orderings, + limit, + perPartitionLimit, + allowsFiltering); + } + + @NonNull + @Override + public SelectFrom distinct() { + return new DefaultSelect( + keyspace, + table, + isJson, + true, + selectors, + relations, + groupByClauses, + orderings, + limit, + perPartitionLimit, + allowsFiltering); + } + + @NonNull + @Override + public Select selector(@NonNull Selector selector) { + ImmutableList newSelectors; + if (selector == AllSelector.INSTANCE) { + // '*' cancels any previous one + newSelectors = SELECT_ALL; + } else if (SELECT_ALL.equals(selectors)) { + // previous '*' gets cancelled + newSelectors = ImmutableList.of(selector); + } else { + newSelectors = ImmutableCollections.append(selectors, selector); + } + return withSelectors(newSelectors); + } + + @NonNull + @Override + public Select selectors(@NonNull Iterable additionalSelectors) { + ImmutableList.Builder newSelectors = ImmutableList.builder(); + if (!SELECT_ALL.equals(selectors)) { // previous '*' gets cancelled + newSelectors.addAll(selectors); + } + for (Selector selector : additionalSelectors) { + if (selector == AllSelector.INSTANCE) { + throw new IllegalArgumentException("Can't pass the * selector to selectors()"); + } + newSelectors.add(selector); + } + return withSelectors(newSelectors.build()); + } + + @NonNull + @Override + public Select as(@NonNull CqlIdentifier alias) { + if (SELECT_ALL.equals(selectors)) { + throw new IllegalStateException("Can't alias the * selector"); + } else if (selectors.isEmpty()) { + throw new IllegalStateException("Can't alias, no selectors defined"); + } + return withSelectors(ImmutableCollections.modifyLast(selectors, last -> last.as(alias))); + } + + @NonNull + public Select withSelectors(@NonNull ImmutableList newSelectors) { + return new DefaultSelect( + keyspace, + table, + isJson, + isDistinct, + newSelectors, + relations, + groupByClauses, + orderings, + limit, + perPartitionLimit, + allowsFiltering); + } + + @NonNull + @Override + public Select where(@NonNull Relation relation) { + return withRelations(ImmutableCollections.append(relations, relation)); + } + + @NonNull + @Override + public Select where(@NonNull Iterable additionalRelations) { + return withRelations(ImmutableCollections.concat(relations, additionalRelations)); + } + + @NonNull + public Select withRelations(@NonNull ImmutableList newRelations) { + return new DefaultSelect( + keyspace, + table, + isJson, + isDistinct, + selectors, + newRelations, + groupByClauses, + orderings, + limit, + perPartitionLimit, + allowsFiltering); + } + + @NonNull + @Override + public Select groupBy(@NonNull Selector groupByClause) { + return withGroupByClauses(ImmutableCollections.append(groupByClauses, groupByClause)); + } + + @NonNull + @Override + public Select groupBy(@NonNull Iterable newGroupByClauses) { + return withGroupByClauses(ImmutableCollections.concat(groupByClauses, newGroupByClauses)); + } + + @NonNull + public Select withGroupByClauses(@NonNull ImmutableList newGroupByClauses) { + return new DefaultSelect( + keyspace, + table, + isJson, + isDistinct, + selectors, + relations, + newGroupByClauses, + orderings, + limit, + perPartitionLimit, + allowsFiltering); + } + + @NonNull + @Override + public Select orderBy(@NonNull CqlIdentifier columnId, @NonNull ClusteringOrder order) { + return withOrderings(ImmutableCollections.append(orderings, columnId, order)); + } + + @NonNull + @Override + public Select orderByIds(@NonNull Map newOrderings) { + return withOrderings(ImmutableCollections.concat(orderings, newOrderings)); + } + + @NonNull + public Select withOrderings(@NonNull ImmutableMap newOrderings) { + return new DefaultSelect( + keyspace, + table, + isJson, + isDistinct, + selectors, + relations, + groupByClauses, + newOrderings, + limit, + perPartitionLimit, + allowsFiltering); + } + + @NonNull + @Override + public Select limit(int limit) { + Preconditions.checkArgument(limit > 0, "Limit must be strictly positive"); + return new DefaultSelect( + keyspace, + table, + isJson, + isDistinct, + selectors, + relations, + groupByClauses, + orderings, + limit, + perPartitionLimit, + allowsFiltering); + } + + @NonNull + @Override + public Select limit(@Nullable BindMarker bindMarker) { + return new DefaultSelect( + keyspace, + table, + isJson, + isDistinct, + selectors, + relations, + groupByClauses, + orderings, + bindMarker, + perPartitionLimit, + allowsFiltering); + } + + @NonNull + @Override + public Select perPartitionLimit(int perPartitionLimit) { + Preconditions.checkArgument( + perPartitionLimit > 0, "perPartitionLimit must be strictly positive"); + return new DefaultSelect( + keyspace, + table, + isJson, + isDistinct, + selectors, + relations, + groupByClauses, + orderings, + limit, + perPartitionLimit, + allowsFiltering); + } + + @NonNull + @Override + public Select perPartitionLimit(@Nullable BindMarker bindMarker) { + return new DefaultSelect( + keyspace, + table, + isJson, + isDistinct, + selectors, + relations, + groupByClauses, + orderings, + limit, + bindMarker, + allowsFiltering); + } + + @NonNull + @Override + public Select allowFiltering() { + return new DefaultSelect( + keyspace, + table, + isJson, + isDistinct, + selectors, + relations, + groupByClauses, + orderings, + limit, + perPartitionLimit, + true); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder(); + + builder.append("SELECT"); + if (isJson) { + builder.append(" JSON"); + } + if (isDistinct) { + builder.append(" DISTINCT"); + } + + CqlHelper.append(selectors, builder, " ", ",", null); + + builder.append(" FROM "); + CqlHelper.qualify(keyspace, table, builder); + + CqlHelper.append(relations, builder, " WHERE ", " AND ", null); + CqlHelper.append(groupByClauses, builder, " GROUP BY ", ",", null); + + boolean first = true; + for (Map.Entry entry : orderings.entrySet()) { + if (first) { + builder.append(" ORDER BY "); + first = false; + } else { + builder.append(","); + } + builder.append(entry.getKey().asCql(true)).append(" ").append(entry.getValue().name()); + } + + if (limit != null) { + builder.append(" LIMIT "); + if (limit instanceof BindMarker) { + ((BindMarker) limit).appendTo(builder); + } else { + builder.append(limit); + } + } + + if (perPartitionLimit != null) { + builder.append(" PER PARTITION LIMIT "); + if (perPartitionLimit instanceof BindMarker) { + ((BindMarker) perPartitionLimit).appendTo(builder); + } else { + builder.append(perPartitionLimit); + } + } + + if (allowsFiltering) { + builder.append(" ALLOW FILTERING"); + } + + return builder.toString(); + } + + @NonNull + @Override + public SimpleStatement build() { + return builder().build(); + } + + @NonNull + @Override + public SimpleStatement build(@NonNull Object... values) { + return builder().addPositionalValues(values).build(); + } + + @NonNull + @Override + public SimpleStatement build(@NonNull Map namedValues) { + SimpleStatementBuilder builder = builder(); + for (Map.Entry entry : namedValues.entrySet()) { + builder.addNamedValue(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + + @NonNull + @Override + public SimpleStatementBuilder builder() { + // SELECT statements are always idempotent + return SimpleStatement.builder(asCql()).setIdempotence(true); + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getTable() { + return table; + } + + public boolean isJson() { + return isJson; + } + + public boolean isDistinct() { + return isDistinct; + } + + @NonNull + public ImmutableList getSelectors() { + return selectors; + } + + @NonNull + public ImmutableList getRelations() { + return relations; + } + + @NonNull + public ImmutableList getGroupByClauses() { + return groupByClauses; + } + + @NonNull + public ImmutableMap getOrderings() { + return orderings; + } + + @Nullable + public Object getLimit() { + return limit; + } + + @Nullable + public Object getPerPartitionLimit() { + return perPartitionLimit; + } + + public boolean allowsFiltering() { + return allowsFiltering; + } + + @Override + public String toString() { + return asCql(); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ElementSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ElementSelector.java new file mode 100644 index 00000000000..1577fb30b9f --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ElementSelector.java @@ -0,0 +1,98 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class ElementSelector implements Selector { + + private final Selector collection; + private final Term index; + private final CqlIdentifier alias; + + public ElementSelector(@NonNull Selector collection, @NonNull Term index) { + this(collection, index, null); + } + + public ElementSelector( + @NonNull Selector collection, @NonNull Term index, @Nullable CqlIdentifier alias) { + Preconditions.checkNotNull(collection); + Preconditions.checkNotNull(index); + this.collection = collection; + this.index = index; + this.alias = alias; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new ElementSelector(collection, index, alias); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + collection.appendTo(builder); + builder.append('['); + index.appendTo(builder); + builder.append(']'); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @NonNull + public Selector getCollection() { + return collection; + } + + @NonNull + public Term getIndex() { + return index; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof ElementSelector) { + ElementSelector that = (ElementSelector) other; + return this.collection.equals(that.collection) + && this.index.equals(that.index) + && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(collection, index, alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/FieldSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/FieldSelector.java new file mode 100644 index 00000000000..6147903f633 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/FieldSelector.java @@ -0,0 +1,95 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class FieldSelector implements Selector { + + private final Selector udt; + private final CqlIdentifier fieldId; + private final CqlIdentifier alias; + + public FieldSelector(@NonNull Selector udt, @NonNull CqlIdentifier fieldId) { + this(udt, fieldId, null); + } + + public FieldSelector( + @NonNull Selector udt, @NonNull CqlIdentifier fieldId, @Nullable CqlIdentifier alias) { + Preconditions.checkNotNull(udt); + Preconditions.checkNotNull(fieldId); + this.udt = udt; + this.fieldId = fieldId; + this.alias = alias; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new FieldSelector(udt, fieldId, alias); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + udt.appendTo(builder); + builder.append('.').append(fieldId.asCql(true)); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @NonNull + public Selector getUdt() { + return udt; + } + + @NonNull + public CqlIdentifier getFieldId() { + return fieldId; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof FieldSelector) { + FieldSelector that = (FieldSelector) other; + return this.udt.equals(that.udt) + && this.fieldId.equals(that.fieldId) + && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(udt, fieldId, alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/FunctionSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/FunctionSelector.java new file mode 100644 index 00000000000..3642008405f --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/FunctionSelector.java @@ -0,0 +1,76 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class FunctionSelector extends CollectionSelector { + + private final CqlIdentifier keyspaceId; + private final CqlIdentifier functionId; + + public FunctionSelector( + @Nullable CqlIdentifier keyspaceId, + @NonNull CqlIdentifier functionId, + @NonNull Iterable arguments) { + this(keyspaceId, functionId, arguments, null); + } + + public FunctionSelector( + @Nullable CqlIdentifier keyspaceId, + @NonNull CqlIdentifier functionId, + @NonNull Iterable elementSelectors, + @Nullable CqlIdentifier alias) { + super(elementSelectors, buildOpening(keyspaceId, functionId), ")", alias); + this.keyspaceId = keyspaceId; + this.functionId = functionId; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new FunctionSelector(keyspaceId, functionId, getElementSelectors(), alias); + } + + @Nullable + public CqlIdentifier getKeyspaceId() { + return keyspaceId; + } + + @NonNull + public CqlIdentifier getFunctionId() { + return functionId; + } + + /** Returns the arguments of the function. */ + @NonNull + @Override + public Iterable getElementSelectors() { + // Overridden only to customize the javadoc + return super.getElementSelectors(); + } + + private static String buildOpening(CqlIdentifier keyspaceId, CqlIdentifier functionId) { + return (keyspaceId == null) + ? functionId.asCql(true) + "(" + : keyspaceId.asCql(true) + "." + functionId.asCql(true) + "("; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ListSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ListSelector.java new file mode 100644 index 00000000000..2d62fe38f59 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/ListSelector.java @@ -0,0 +1,40 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class ListSelector extends CollectionSelector { + + public ListSelector(@NonNull Iterable elementSelectors) { + this(elementSelectors, null); + } + + public ListSelector(@NonNull Iterable elementSelectors, @Nullable CqlIdentifier alias) { + super(elementSelectors, "[", "]", alias); + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new ListSelector(getElementSelectors(), alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/MapSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/MapSelector.java new file mode 100644 index 00000000000..f7fde80d5bd --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/MapSelector.java @@ -0,0 +1,159 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class MapSelector implements Selector { + + private final Map elementSelectors; + private final DataType keyType; + private final DataType valueType; + private final CqlIdentifier alias; + + public MapSelector( + @NonNull Map elementSelectors, + @Nullable DataType keyType, + @Nullable DataType valueType) { + this(elementSelectors, keyType, valueType, null); + } + + public MapSelector( + @NonNull Map elementSelectors, + @Nullable DataType keyType, + @Nullable DataType valueType, + @Nullable CqlIdentifier alias) { + Preconditions.checkNotNull(elementSelectors); + Preconditions.checkArgument( + !elementSelectors.isEmpty(), "Must have at least one key/value pair"); + checkNoAlias(elementSelectors); + Preconditions.checkArgument( + (keyType == null) == (valueType == null), + "Key and value type must be either both null or both non-null"); + this.elementSelectors = elementSelectors; + this.keyType = keyType; + this.valueType = valueType; + this.alias = alias; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new MapSelector(elementSelectors, keyType, valueType, alias); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + if (keyType != null) { + assert valueType != null; + builder + .append("(map<") + .append(keyType.asCql(false, true)) + .append(',') + .append(valueType.asCql(false, true)) + .append(">)"); + } + builder.append("{"); + boolean first = true; + for (Map.Entry entry : elementSelectors.entrySet()) { + if (first) { + first = false; + } else { + builder.append(","); + } + entry.getKey().appendTo(builder); + builder.append(":"); + entry.getValue().appendTo(builder); + } + builder.append("}"); + + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @NonNull + public Map getElementSelectors() { + return elementSelectors; + } + + @Nullable + public DataType getKeyType() { + return keyType; + } + + @Nullable + public DataType getValueType() { + return valueType; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof MapSelector) { + MapSelector that = (MapSelector) other; + return this.elementSelectors.equals(that.elementSelectors) + && Objects.equals(this.keyType, that.keyType) + && Objects.equals(this.valueType, that.valueType) + && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(elementSelectors, keyType, valueType, alias); + } + + private static void checkNoAlias(Map elementSelectors) { + String offendingAliases = null; + for (Map.Entry entry : elementSelectors.entrySet()) { + offendingAliases = appendIfNotNull(offendingAliases, entry.getKey().getAlias()); + offendingAliases = appendIfNotNull(offendingAliases, entry.getValue().getAlias()); + } + if (offendingAliases != null) { + throw new IllegalArgumentException( + "Can't use aliases in selection map, offending aliases: " + offendingAliases); + } + } + + private static String appendIfNotNull(String offendingAliases, CqlIdentifier alias) { + if (alias == null) { + return offendingAliases; + } else if (offendingAliases == null) { + return alias.asCql(true); + } else { + return offendingAliases + ", " + alias.asCql(true); + } + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/OppositeSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/OppositeSelector.java new file mode 100644 index 00000000000..b6a18cf51f5 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/OppositeSelector.java @@ -0,0 +1,86 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.internal.querybuilder.ArithmeticOperator; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class OppositeSelector extends ArithmeticSelector { + + private final Selector argument; + private final CqlIdentifier alias; + + public OppositeSelector(@NonNull Selector argument) { + this(argument, null); + } + + public OppositeSelector(@NonNull Selector argument, @Nullable CqlIdentifier alias) { + super(ArithmeticOperator.OPPOSITE); + Preconditions.checkNotNull(argument); + this.argument = argument; + this.alias = alias; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new OppositeSelector(argument, alias); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append('-'); + appendAndMaybeParenthesize(operator.getPrecedenceLeft(), argument, builder); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @NonNull + public Selector getArgument() { + return argument; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof OppositeSelector) { + OppositeSelector that = (OppositeSelector) other; + return this.argument.equals(that.argument) && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(argument, alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/RangeSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/RangeSelector.java new file mode 100644 index 00000000000..a21a8d0c59e --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/RangeSelector.java @@ -0,0 +1,116 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class RangeSelector implements Selector { + + private final Selector collection; + private final Term left; + private final Term right; + private final CqlIdentifier alias; + + public RangeSelector(@NonNull Selector collection, @Nullable Term left, @Nullable Term right) { + this(collection, left, right, null); + } + + public RangeSelector( + @NonNull Selector collection, + @Nullable Term left, + @Nullable Term right, + @Nullable CqlIdentifier alias) { + Preconditions.checkNotNull(collection); + Preconditions.checkArgument( + left != null || right != null, "At least one of the bounds must be specified"); + this.collection = collection; + this.left = left; + this.right = right; + this.alias = alias; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new RangeSelector(collection, left, right, alias); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + collection.appendTo(builder); + builder.append('['); + if (left != null) { + left.appendTo(builder); + } + builder.append(".."); + if (right != null) { + right.appendTo(builder); + } + builder.append(']'); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @NonNull + public Selector getCollection() { + return collection; + } + + @Nullable + public Term getLeft() { + return left; + } + + @Nullable + public Term getRight() { + return right; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof RangeSelector) { + RangeSelector that = (RangeSelector) other; + return this.collection.equals(that.collection) + && Objects.equals(this.left, that.left) + && Objects.equals(this.right, that.right) + && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(collection, left, right, alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/SetSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/SetSelector.java new file mode 100644 index 00000000000..a291247d546 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/SetSelector.java @@ -0,0 +1,40 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class SetSelector extends CollectionSelector { + + public SetSelector(@NonNull Iterable elementSelectors) { + this(elementSelectors, null); + } + + public SetSelector(@NonNull Iterable elementSelectors, @Nullable CqlIdentifier alias) { + super(elementSelectors, "{", "}", alias); + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new SetSelector(getElementSelectors(), alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/TupleSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/TupleSelector.java new file mode 100644 index 00000000000..2058313eabb --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/TupleSelector.java @@ -0,0 +1,41 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class TupleSelector extends CollectionSelector { + + public TupleSelector(@NonNull Iterable elementSelectors) { + this(elementSelectors, null); + } + + public TupleSelector( + @NonNull Iterable elementSelectors, @Nullable CqlIdentifier alias) { + super(elementSelectors, "(", ")", alias); + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new TupleSelector(getElementSelectors(), alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/TypeHintSelector.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/TypeHintSelector.java new file mode 100644 index 00000000000..3f8f15ba729 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/select/TypeHintSelector.java @@ -0,0 +1,96 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.select; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.select.Selector; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class TypeHintSelector implements Selector { + + private final Selector selector; + private final DataType targetType; + private final CqlIdentifier alias; + + public TypeHintSelector(@NonNull Selector selector, @NonNull DataType targetType) { + this(selector, targetType, null); + } + + public TypeHintSelector( + @NonNull Selector selector, @NonNull DataType targetType, @Nullable CqlIdentifier alias) { + Preconditions.checkNotNull(selector); + Preconditions.checkNotNull(targetType); + this.selector = selector; + this.targetType = targetType; + this.alias = alias; + } + + @NonNull + @Override + public Selector as(@NonNull CqlIdentifier alias) { + return new TypeHintSelector(selector, targetType, alias); + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append('(').append(targetType.asCql(false, true)).append(')'); + selector.appendTo(builder); + if (alias != null) { + builder.append(" AS ").append(alias.asCql(true)); + } + } + + @NonNull + public Selector getSelector() { + return selector; + } + + @NonNull + public DataType getTargetType() { + return targetType; + } + + @Nullable + @Override + public CqlIdentifier getAlias() { + return alias; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof TypeHintSelector) { + TypeHintSelector that = (TypeHintSelector) other; + return this.selector.equals(that.selector) + && this.targetType.equals(that.targetType) + && Objects.equals(this.alias, that.alias); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(selector, targetType, alias); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/ArithmeticTerm.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/ArithmeticTerm.java new file mode 100644 index 00000000000..fdb1c2210ae --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/ArithmeticTerm.java @@ -0,0 +1,52 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.term; + +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.ArithmeticOperator; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public abstract class ArithmeticTerm implements Term { + + protected final ArithmeticOperator operator; + + protected ArithmeticTerm(@NonNull ArithmeticOperator operator) { + Preconditions.checkNotNull(operator); + this.operator = operator; + } + + @NonNull + public ArithmeticOperator getOperator() { + return operator; + } + + protected static void appendAndMaybeParenthesize( + int myPrecedence, @NonNull Term child, @NonNull StringBuilder builder) { + boolean parenthesize = + (child instanceof ArithmeticTerm) + && (((ArithmeticTerm) child).operator.getPrecedenceLeft() < myPrecedence); + if (parenthesize) { + builder.append('('); + } + child.appendTo(builder); + if (parenthesize) { + builder.append(')'); + } + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/BinaryArithmeticTerm.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/BinaryArithmeticTerm.java new file mode 100644 index 00000000000..a29c6cbfc53 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/BinaryArithmeticTerm.java @@ -0,0 +1,80 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.term; + +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.ArithmeticOperator; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import net.jcip.annotations.Immutable; + +@Immutable +public class BinaryArithmeticTerm extends ArithmeticTerm { + + private final Term left; + private final Term right; + + public BinaryArithmeticTerm( + @NonNull ArithmeticOperator operator, @NonNull Term left, @NonNull Term right) { + super(operator); + Preconditions.checkNotNull(left); + Preconditions.checkNotNull(right); + this.left = left; + this.right = right; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + appendAndMaybeParenthesize(operator.getPrecedenceLeft(), left, builder); + builder.append(operator.getSymbol()); + appendAndMaybeParenthesize(operator.getPrecedenceRight(), right, builder); + } + + @Override + public boolean isIdempotent() { + return left.isIdempotent() && right.isIdempotent(); + } + + @NonNull + public Term getLeft() { + return left; + } + + @NonNull + public Term getRight() { + return right; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof BinaryArithmeticTerm) { + BinaryArithmeticTerm that = (BinaryArithmeticTerm) other; + return this.operator.equals(that.operator) + && this.left.equals(that.left) + && this.right.equals(that.right); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(operator, left, right); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/FunctionTerm.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/FunctionTerm.java new file mode 100644 index 00000000000..e91ed04b775 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/FunctionTerm.java @@ -0,0 +1,72 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.term; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class FunctionTerm implements Term { + + private final CqlIdentifier keyspaceId; + private final CqlIdentifier functionId; + private final Iterable arguments; + + public FunctionTerm( + @Nullable CqlIdentifier keyspaceId, + @NonNull CqlIdentifier functionId, + @NonNull Iterable arguments) { + Preconditions.checkNotNull(functionId); + Preconditions.checkNotNull(arguments); + this.keyspaceId = keyspaceId; + this.functionId = functionId; + this.arguments = arguments; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + // The function name appears even without arguments, so don't use prefix/suffix in CqlHelper + CqlHelper.qualify(keyspaceId, functionId, builder); + builder.append('('); + CqlHelper.append(arguments, builder, null, ",", null); + builder.append(')'); + } + + @Override + public boolean isIdempotent() { + return false; + } + + @Nullable + public CqlIdentifier getKeyspaceId() { + return keyspaceId; + } + + @NonNull + public CqlIdentifier getFunctionId() { + return functionId; + } + + @NonNull + public Iterable getArguments() { + return arguments; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/OppositeTerm.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/OppositeTerm.java new file mode 100644 index 00000000000..750bea39167 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/OppositeTerm.java @@ -0,0 +1,67 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.term; + +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.ArithmeticOperator; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class OppositeTerm extends ArithmeticTerm { + + @NonNull private final Term argument; + + public OppositeTerm(@NonNull Term argument) { + super(ArithmeticOperator.OPPOSITE); + Preconditions.checkNotNull(argument); + this.argument = argument; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append('-'); + appendAndMaybeParenthesize(operator.getPrecedenceLeft(), argument, builder); + } + + @Override + public boolean isIdempotent() { + return argument.isIdempotent(); + } + + @NonNull + public Term getArgument() { + return argument; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (other instanceof OppositeTerm) { + OppositeTerm that = (OppositeTerm) other; + return this.argument.equals(that.argument); + } else { + return false; + } + } + + @Override + public int hashCode() { + return argument.hashCode(); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/TupleTerm.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/TupleTerm.java new file mode 100644 index 00000000000..c9c14dab3ec --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/TupleTerm.java @@ -0,0 +1,51 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.term; + +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class TupleTerm implements Term { + + private final Iterable components; + + public TupleTerm(@NonNull Iterable components) { + this.components = components; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + CqlHelper.append(components, builder, "(", ",", ")"); + } + + @Override + public boolean isIdempotent() { + for (Term component : components) { + if (!component.isIdempotent()) { + return false; + } + } + return true; + } + + @NonNull + public Iterable getComponents() { + return components; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/TypeHintTerm.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/TypeHintTerm.java new file mode 100644 index 00000000000..3ba9b53eb9f --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/term/TypeHintTerm.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.term; + +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class TypeHintTerm implements Term { + + private final Term term; + private final DataType targetType; + + public TypeHintTerm(@NonNull Term term, @NonNull DataType targetType) { + this.term = term; + this.targetType = targetType; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append('(').append(targetType.asCql(false, true)).append(')'); + term.appendTo(builder); + } + + @Override + public boolean isIdempotent() { + return term.isIdempotent(); + } + + @NonNull + public Term getTerm() { + return term; + } + + @NonNull + public DataType getTargetType() { + return targetType; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendAssignment.java new file mode 100644 index 00000000000..271c0bcca16 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendAssignment.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.lhs.LeftOperand; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class AppendAssignment extends DefaultAssignment { + + public AppendAssignment(@NonNull LeftOperand leftOperand, @NonNull Term rightOperand) { + super(leftOperand, "+=", rightOperand); + } + + @Override + public boolean isIdempotent() { + // Not idempotent for lists, be pessimistic + return false; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendListElementAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendListElementAssignment.java new file mode 100644 index 00000000000..0005efaf7e2 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendListElementAssignment.java @@ -0,0 +1,34 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class AppendListElementAssignment extends CollectionElementAssignment { + + public AppendListElementAssignment(@NonNull CqlIdentifier columnId, @NonNull Term element) { + super(columnId, Operator.APPEND, null, element, '[', ']'); + } + + @Override + public boolean isIdempotent() { + return false; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendMapEntryAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendMapEntryAssignment.java new file mode 100644 index 00000000000..c04c6e23b7e --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendMapEntryAssignment.java @@ -0,0 +1,30 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class AppendMapEntryAssignment extends CollectionElementAssignment { + + public AppendMapEntryAssignment( + @NonNull CqlIdentifier columnId, @NonNull Term key, @NonNull Term value) { + super(columnId, Operator.APPEND, key, value, '{', '}'); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendSetElementAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendSetElementAssignment.java new file mode 100644 index 00000000000..6edf7108d76 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/AppendSetElementAssignment.java @@ -0,0 +1,29 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class AppendSetElementAssignment extends CollectionElementAssignment { + + public AppendSetElementAssignment(@NonNull CqlIdentifier columnId, @NonNull Term element) { + super(columnId, Operator.APPEND, null, element, '{', '}'); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/CollectionElementAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/CollectionElementAssignment.java new file mode 100644 index 00000000000..35da1ca0f6e --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/CollectionElementAssignment.java @@ -0,0 +1,109 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.api.querybuilder.update.Assignment; +import com.datastax.oss.driver.shaded.guava.common.base.Preconditions; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public abstract class CollectionElementAssignment implements Assignment { + + public enum Operator { + APPEND("%s+=%s"), + PREPEND("%1$s=%2$s+%1$s"), + REMOVE("%s-=%s"), + ; + + public final String pattern; + + Operator(String pattern) { + this.pattern = pattern; + } + } + + private final CqlIdentifier columnId; + private final Operator operator; + private final Term key; + private final Term value; + private final char opening; + private final char closing; + + protected CollectionElementAssignment( + @NonNull CqlIdentifier columnId, + @NonNull Operator operator, + @Nullable Term key, + @NonNull Term value, + char opening, + char closing) { + Preconditions.checkNotNull(columnId); + Preconditions.checkNotNull(value); + this.columnId = columnId; + this.operator = operator; + this.key = key; + this.value = value; + this.opening = opening; + this.closing = closing; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + builder.append(String.format(operator.pattern, columnId.asCql(true), buildRightOperand())); + } + + private String buildRightOperand() { + StringBuilder builder = new StringBuilder(); + builder.append(opening); + if (key != null) { + key.appendTo(builder); + builder.append(':'); + } + value.appendTo(builder); + return builder.append(closing).toString(); + } + + @Override + public boolean isIdempotent() { + return (key == null || key.isIdempotent()) && value.isIdempotent(); + } + + @NonNull + public CqlIdentifier getColumnId() { + return columnId; + } + + @Nullable + public Term getKey() { + return key; + } + + @NonNull + public Term getValue() { + return value; + } + + public char getOpening() { + return opening; + } + + public char getClosing() { + return closing; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/CounterAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/CounterAssignment.java new file mode 100644 index 00000000000..ff1280de5dd --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/CounterAssignment.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.internal.querybuilder.lhs.LeftOperand; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class CounterAssignment extends DefaultAssignment { + + public CounterAssignment( + @NonNull LeftOperand leftOperand, @NonNull String operator, @NonNull Term rightOperand) { + super(leftOperand, operator, rightOperand); + } + + @Override + public boolean isIdempotent() { + return false; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/DefaultAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/DefaultAssignment.java new file mode 100644 index 00000000000..b889831d5fd --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/DefaultAssignment.java @@ -0,0 +1,67 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.api.querybuilder.update.Assignment; +import com.datastax.oss.driver.internal.querybuilder.lhs.LeftOperand; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultAssignment implements Assignment { + + private final LeftOperand leftOperand; + private final String operator; + private final Term rightOperand; + + public DefaultAssignment( + @NonNull LeftOperand leftOperand, @NonNull String operator, @Nullable Term rightOperand) { + this.leftOperand = leftOperand; + this.operator = operator; + this.rightOperand = rightOperand; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + leftOperand.appendTo(builder); + builder.append(operator); + if (rightOperand != null) { + rightOperand.appendTo(builder); + } + } + + @Override + public boolean isIdempotent() { + return rightOperand == null || rightOperand.isIdempotent(); + } + + @NonNull + public LeftOperand getLeftOperand() { + return leftOperand; + } + + @NonNull + public String getOperator() { + return operator; + } + + @Nullable + public Term getRightOperand() { + return rightOperand; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/DefaultUpdate.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/DefaultUpdate.java new file mode 100644 index 00000000000..5428f5b3b01 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/DefaultUpdate.java @@ -0,0 +1,253 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.SimpleStatementBuilder; +import com.datastax.oss.driver.api.querybuilder.BindMarker; +import com.datastax.oss.driver.api.querybuilder.condition.Condition; +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.api.querybuilder.update.Assignment; +import com.datastax.oss.driver.api.querybuilder.update.Update; +import com.datastax.oss.driver.api.querybuilder.update.UpdateStart; +import com.datastax.oss.driver.api.querybuilder.update.UpdateWithAssignments; +import com.datastax.oss.driver.internal.querybuilder.CqlHelper; +import com.datastax.oss.driver.internal.querybuilder.ImmutableCollections; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.Map; +import net.jcip.annotations.Immutable; + +@Immutable +public class DefaultUpdate implements UpdateStart, UpdateWithAssignments, Update { + + private final CqlIdentifier keyspace; + private final CqlIdentifier table; + private final Object timestamp; + private final ImmutableList assignments; + private final ImmutableList relations; + private final boolean ifExists; + private final ImmutableList conditions; + + public DefaultUpdate(@Nullable CqlIdentifier keyspace, @NonNull CqlIdentifier table) { + this(keyspace, table, null, ImmutableList.of(), ImmutableList.of(), false, ImmutableList.of()); + } + + public DefaultUpdate( + @Nullable CqlIdentifier keyspace, + @NonNull CqlIdentifier table, + @Nullable Object timestamp, + @NonNull ImmutableList assignments, + @NonNull ImmutableList relations, + boolean ifExists, + @NonNull ImmutableList conditions) { + this.keyspace = keyspace; + this.table = table; + this.timestamp = timestamp; + this.assignments = assignments; + this.relations = relations; + this.ifExists = ifExists; + this.conditions = conditions; + } + + @NonNull + @Override + public UpdateStart usingTimestamp(long newTimestamp) { + return new DefaultUpdate( + keyspace, table, newTimestamp, assignments, relations, ifExists, conditions); + } + + @NonNull + @Override + public UpdateStart usingTimestamp(@NonNull BindMarker newTimestamp) { + return new DefaultUpdate( + keyspace, table, newTimestamp, assignments, relations, ifExists, conditions); + } + + @NonNull + @Override + public UpdateWithAssignments set(@NonNull Assignment assignment) { + return withAssignments(ImmutableCollections.append(assignments, assignment)); + } + + @NonNull + @Override + public UpdateWithAssignments set(@NonNull Iterable additionalAssignments) { + return withAssignments(ImmutableCollections.concat(assignments, additionalAssignments)); + } + + @NonNull + public UpdateWithAssignments withAssignments(@NonNull ImmutableList newAssignments) { + return new DefaultUpdate( + keyspace, table, timestamp, newAssignments, relations, ifExists, conditions); + } + + @NonNull + @Override + public Update where(@NonNull Relation relation) { + return withRelations(ImmutableCollections.append(relations, relation)); + } + + @NonNull + @Override + public Update where(@NonNull Iterable additionalRelations) { + return withRelations(ImmutableCollections.concat(relations, additionalRelations)); + } + + @NonNull + public Update withRelations(@NonNull ImmutableList newRelations) { + return new DefaultUpdate( + keyspace, table, timestamp, assignments, newRelations, ifExists, conditions); + } + + @NonNull + @Override + public Update ifExists() { + return new DefaultUpdate(keyspace, table, timestamp, assignments, relations, true, conditions); + } + + @NonNull + @Override + public Update if_(@NonNull Condition condition) { + return withConditions(ImmutableCollections.append(conditions, condition)); + } + + @NonNull + @Override + public Update if_(@NonNull Iterable additionalConditions) { + return withConditions(ImmutableCollections.concat(conditions, additionalConditions)); + } + + @NonNull + public Update withConditions(@NonNull ImmutableList newConditions) { + return new DefaultUpdate( + keyspace, table, timestamp, assignments, relations, false, newConditions); + } + + @NonNull + @Override + public String asCql() { + StringBuilder builder = new StringBuilder("UPDATE "); + CqlHelper.qualify(keyspace, table, builder); + + if (timestamp != null) { + builder.append(" USING TIMESTAMP "); + if (timestamp instanceof BindMarker) { + ((BindMarker) timestamp).appendTo(builder); + } else { + builder.append(timestamp); + } + } + + CqlHelper.append(assignments, builder, " SET ", ", ", null); + CqlHelper.append(relations, builder, " WHERE ", " AND ", null); + + if (ifExists) { + builder.append(" IF EXISTS"); + } else { + CqlHelper.append(conditions, builder, " IF ", " AND ", null); + } + return builder.toString(); + } + + @NonNull + @Override + public SimpleStatement build() { + return builder().build(); + } + + @NonNull + @Override + public SimpleStatement build(@NonNull Object... values) { + return builder().addPositionalValues(values).build(); + } + + @NonNull + @Override + public SimpleStatement build(@NonNull Map namedValues) { + SimpleStatementBuilder builder = builder(); + for (Map.Entry entry : namedValues.entrySet()) { + builder.addNamedValue(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + + @NonNull + @Override + public SimpleStatementBuilder builder() { + return SimpleStatement.builder(asCql()).setIdempotence(isIdempotent()); + } + + public boolean isIdempotent() { + // Conditional queries are never idempotent, see JAVA-819 + if (!conditions.isEmpty() || ifExists) { + return false; + } else { + for (Assignment assignment : assignments) { + if (!assignment.isIdempotent()) { + return false; + } + } + for (Relation relation : relations) { + if (!relation.isIdempotent()) { + return false; + } + } + return true; + } + } + + @Nullable + public CqlIdentifier getKeyspace() { + return keyspace; + } + + @NonNull + public CqlIdentifier getTable() { + return table; + } + + @Nullable + public Object getTimestamp() { + return timestamp; + } + + @NonNull + public ImmutableList getAssignments() { + return assignments; + } + + @NonNull + public ImmutableList getRelations() { + return relations; + } + + public boolean isIfExists() { + return ifExists; + } + + @NonNull + public ImmutableList getConditions() { + return conditions; + } + + @Override + public String toString() { + return asCql(); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependAssignment.java new file mode 100644 index 00000000000..d58a6ffa18e --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependAssignment.java @@ -0,0 +1,58 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import com.datastax.oss.driver.api.querybuilder.update.Assignment; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class PrependAssignment implements Assignment { + + private final CqlIdentifier columnId; + private final Term prefix; + + public PrependAssignment(@NonNull CqlIdentifier columnId, @NonNull Term prefix) { + this.columnId = columnId; + this.prefix = prefix; + } + + @Override + public void appendTo(@NonNull StringBuilder builder) { + String column = columnId.asCql(true); + builder.append(column).append('='); + prefix.appendTo(builder); + builder.append('+').append(column); + } + + @Override + public boolean isIdempotent() { + // Not idempotent for lists, be pessimistic + return false; + } + + @NonNull + public CqlIdentifier getColumnId() { + return columnId; + } + + @NonNull + public Term getPrefix() { + return prefix; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependListElementAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependListElementAssignment.java new file mode 100644 index 00000000000..a9bc032c432 --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependListElementAssignment.java @@ -0,0 +1,34 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class PrependListElementAssignment extends CollectionElementAssignment { + + public PrependListElementAssignment(@NonNull CqlIdentifier columnId, @NonNull Term element) { + super(columnId, Operator.PREPEND, null, element, '[', ']'); + } + + @Override + public boolean isIdempotent() { + return false; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependMapEntryAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependMapEntryAssignment.java new file mode 100644 index 00000000000..691ab6461be --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependMapEntryAssignment.java @@ -0,0 +1,30 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class PrependMapEntryAssignment extends CollectionElementAssignment { + + public PrependMapEntryAssignment( + @NonNull CqlIdentifier columnId, @NonNull Term key, @NonNull Term value) { + super(columnId, Operator.PREPEND, key, value, '{', '}'); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependSetElementAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependSetElementAssignment.java new file mode 100644 index 00000000000..7924a0d6afe --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/PrependSetElementAssignment.java @@ -0,0 +1,29 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class PrependSetElementAssignment extends CollectionElementAssignment { + + public PrependSetElementAssignment(@NonNull CqlIdentifier columnId, @NonNull Term element) { + super(columnId, Operator.PREPEND, null, element, '{', '}'); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/RemoveListElementAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/RemoveListElementAssignment.java new file mode 100644 index 00000000000..985a871fe5e --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/RemoveListElementAssignment.java @@ -0,0 +1,34 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class RemoveListElementAssignment extends CollectionElementAssignment { + + public RemoveListElementAssignment(@NonNull CqlIdentifier columnId, @NonNull Term element) { + super(columnId, Operator.REMOVE, null, element, '[', ']'); + } + + @Override + public boolean isIdempotent() { + return false; + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/RemoveMapEntryAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/RemoveMapEntryAssignment.java new file mode 100644 index 00000000000..c87e30250fc --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/RemoveMapEntryAssignment.java @@ -0,0 +1,30 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class RemoveMapEntryAssignment extends CollectionElementAssignment { + + public RemoveMapEntryAssignment( + @NonNull CqlIdentifier columnId, @NonNull Term key, @NonNull Term value) { + super(columnId, Operator.REMOVE, key, value, '{', '}'); + } +} diff --git a/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/RemoveSetElementAssignment.java b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/RemoveSetElementAssignment.java new file mode 100644 index 00000000000..f863dd7f67f --- /dev/null +++ b/query-builder/src/main/java/com/datastax/oss/driver/internal/querybuilder/update/RemoveSetElementAssignment.java @@ -0,0 +1,29 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.internal.querybuilder.update; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.querybuilder.term.Term; +import edu.umd.cs.findbugs.annotations.NonNull; +import net.jcip.annotations.Immutable; + +@Immutable +public class RemoveSetElementAssignment extends CollectionElementAssignment { + + public RemoveSetElementAssignment(@NonNull CqlIdentifier columnId, @NonNull Term element) { + super(columnId, Operator.REMOVE, null, element, '{', '}'); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/Assertions.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/Assertions.java new file mode 100644 index 00000000000..b931357fd92 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/Assertions.java @@ -0,0 +1,27 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder; + +public class Assertions extends org.assertj.core.api.Assertions { + + public static BuildableQueryAssert assertThat(BuildableQuery actual) { + return new BuildableQueryAssert(actual); + } + + public static CqlSnippetAssert assertThat(CqlSnippet actual) { + return new CqlSnippetAssert(actual); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/BuildableQueryAssert.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/BuildableQueryAssert.java new file mode 100644 index 00000000000..38cc5c9d43a --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/BuildableQueryAssert.java @@ -0,0 +1,42 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.assertj.core.api.AbstractAssert; + +public class BuildableQueryAssert extends AbstractAssert { + + public BuildableQueryAssert(BuildableQuery actual) { + super(actual, BuildableQueryAssert.class); + } + + public BuildableQueryAssert hasCql(String expected) { + assertThat(actual.asCql()).isEqualTo(expected); + return this; + } + + public BuildableQueryAssert isIdempotent() { + assertThat(actual.build().isIdempotent()).isTrue(); + return this; + } + + public BuildableQueryAssert isNotIdempotent() { + assertThat(actual.build().isIdempotent()).isFalse(); + return this; + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/BuildableQueryTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/BuildableQueryTest.java new file mode 100644 index 00000000000..a9ba444072b --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/BuildableQueryTest.java @@ -0,0 +1,153 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder; + +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.function; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.insertInto; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.tuple; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.update; +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(DataProviderRunner.class) +public class BuildableQueryTest { + + @DataProvider + public static Object[][] sampleQueries() { + // query | values | expected CQL | expected idempotence + return new Object[][] { + { + selectFrom("foo").all().whereColumn("k").isEqualTo(bindMarker("k")), + ImmutableMap.of("k", 1), + "SELECT * FROM foo WHERE k=:k", + true + }, + { + deleteFrom("foo").whereColumn("k").isEqualTo(bindMarker("k")), + ImmutableMap.of("k", 1), + "DELETE FROM foo WHERE k=:k", + true + }, + { + deleteFrom("foo").whereColumn("k").isEqualTo(bindMarker("k")).ifExists(), + ImmutableMap.of("k", 1), + "DELETE FROM foo WHERE k=:k IF EXISTS", + false + }, + { + insertInto("foo").value("a", bindMarker("a")).value("b", bindMarker("b")), + ImmutableMap.of("a", 1, "b", "b"), + "INSERT INTO foo (a,b) VALUES (:a,:b)", + true + }, + { + insertInto("foo").value("k", tuple(bindMarker("field1"), function("generate_id"))), + ImmutableMap.of("field1", 1), + "INSERT INTO foo (k) VALUES ((:field1,generate_id()))", + false + }, + { + update("foo").setColumn("v", bindMarker("v")).whereColumn("k").isEqualTo(bindMarker("k")), + ImmutableMap.of("v", 3, "k", 1), + "UPDATE foo SET v=:v WHERE k=:k", + true + }, + { + update("foo") + .setColumn("v", function("non_idempotent_func")) + .whereColumn("k") + .isEqualTo(bindMarker("k")), + ImmutableMap.of("k", 1), + "UPDATE foo SET v=non_idempotent_func() WHERE k=:k", + false + }, + }; + } + + @Test + @UseDataProvider("sampleQueries") + public void should_build_statement_without_values( + BuildableQuery query, + @SuppressWarnings("unused") Map boundValues, + String expectedQueryString, + boolean expectedIdempotence) { + SimpleStatement statement = query.build(); + assertThat(statement.getQuery()).isEqualTo(expectedQueryString); + assertThat(statement.isIdempotent()).isEqualTo(expectedIdempotence); + assertThat(statement.getPositionalValues()).isEmpty(); + assertThat(statement.getNamedValues()).isEmpty(); + } + + @Test + @UseDataProvider("sampleQueries") + public void should_build_statement_with_positional_values( + BuildableQuery query, + Map boundValues, + String expectedQueryString, + boolean expectedIdempotence) { + Object[] positionalValues = boundValues.values().toArray(); + SimpleStatement statement = query.build(positionalValues); + assertThat(statement.getQuery()).isEqualTo(expectedQueryString); + assertThat(statement.isIdempotent()).isEqualTo(expectedIdempotence); + assertThat(statement.getPositionalValues()).containsExactly(positionalValues); + assertThat(statement.getNamedValues()).isEmpty(); + } + + @Test + @UseDataProvider("sampleQueries") + public void should_build_statement_with_named_values( + BuildableQuery query, + Map boundValues, + String expectedQueryString, + boolean expectedIdempotence) { + SimpleStatement statement = query.build(boundValues); + assertThat(statement.getQuery()).isEqualTo(expectedQueryString); + assertThat(statement.isIdempotent()).isEqualTo(expectedIdempotence); + assertThat(statement.getPositionalValues()).isEmpty(); + assertThat(statement.getNamedValues()).hasSize(boundValues.size()); + for (Map.Entry entry : boundValues.entrySet()) { + assertThat(statement.getNamedValues().get(CqlIdentifier.fromCql(entry.getKey()))) + .isEqualTo(entry.getValue()); + } + } + + @Test + @UseDataProvider("sampleQueries") + public void should_convert_to_statement_builder( + BuildableQuery query, + Map boundValues, + String expectedQueryString, + boolean expectedIdempotence) { + Object[] positionalValues = boundValues.values().toArray(); + SimpleStatement statement = query.builder().addPositionalValues(positionalValues).build(); + assertThat(statement.getQuery()).isEqualTo(expectedQueryString); + assertThat(statement.isIdempotent()).isEqualTo(expectedIdempotence); + assertThat(statement.getPositionalValues()).containsExactly(positionalValues); + assertThat(statement.getNamedValues()).isEmpty(); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/CharsetCodec.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/CharsetCodec.java new file mode 100644 index 00000000000..5b16cc80f9b --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/CharsetCodec.java @@ -0,0 +1,69 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.type.DataType; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.TypeCodec; +import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.datastax.oss.driver.internal.core.type.codec.registry.DefaultCodecRegistry; +import com.datastax.oss.driver.internal.querybuilder.DefaultLiteral; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/** Example codec implementation used for {@link DefaultLiteral} tests. */ +public class CharsetCodec implements TypeCodec { + + /** A registry that contains an instance of this codec. */ + public static final CodecRegistry TEST_REGISTRY = + new DefaultCodecRegistry("test", new CharsetCodec()); + + @NonNull + @Override + public GenericType getJavaType() { + return GenericType.of(Charset.class); + } + + @NonNull + @Override + public DataType getCqlType() { + return DataTypes.TEXT; + } + + @NonNull + @Override + public String format(Charset value) { + return "'" + value.name() + "'"; + } + + @Override + public ByteBuffer encode(Charset value, @NonNull ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException("Not used in this test"); + } + + @Override + public Charset decode(ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) { + throw new UnsupportedOperationException("Not used in this test"); + } + + @Override + public Charset parse(String value) { + throw new UnsupportedOperationException("Not used in this test"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/CqlSnippetAssert.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/CqlSnippetAssert.java new file mode 100644 index 00000000000..ee8d41d467d --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/CqlSnippetAssert.java @@ -0,0 +1,34 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.assertj.core.api.AbstractAssert; + +public class CqlSnippetAssert extends AbstractAssert { + + public CqlSnippetAssert(CqlSnippet actual) { + super(actual, CqlSnippetAssert.class); + } + + public CqlSnippetAssert hasCql(String expected) { + StringBuilder builder = new StringBuilder(); + actual.appendTo(builder); + assertThat(builder.toString()).isEqualTo(expected); + return this; + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/condition/ConditionTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/condition/ConditionTest.java new file mode 100644 index 00000000000..06296960a69 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/condition/ConditionTest.java @@ -0,0 +1,65 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.condition; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; + +import org.junit.Test; + +public class ConditionTest { + + @Test + public void should_generate_simple_column_condition() { + deleteFrom("foo").whereColumn("k").isEqualTo(bindMarker()).ifColumn("v").isEqualTo(literal(1)); + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .if_(Condition.column("v").isEqualTo(literal(1)))) + .hasCql("DELETE FROM foo WHERE k=? IF v=1"); + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .if_( + Condition.column("v1").isEqualTo(literal(1)), + Condition.column("v2").isEqualTo(literal(2)))) + .hasCql("DELETE FROM foo WHERE k=? IF v1=1 AND v2=2"); + } + + @Test + public void should_generate_field_condition() { + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .if_(Condition.field("v", "f").isEqualTo(literal(1)))) + .hasCql("DELETE FROM foo WHERE k=? IF v.f=1"); + } + + @Test + public void should_generate_element_condition() { + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .if_(Condition.element("v", literal(1)).isEqualTo(literal(1)))) + .hasCql("DELETE FROM foo WHERE k=? IF v[1]=1"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteFluentConditionTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteFluentConditionTest.java new file mode 100644 index 00000000000..2a75bbaa8d7 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteFluentConditionTest.java @@ -0,0 +1,100 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.delete; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; + +import org.junit.Test; + +public class DeleteFluentConditionTest { + + @Test + public void should_generate_simple_column_condition() { + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifColumn("v") + .isEqualTo(literal(1))) + .hasCql("DELETE FROM foo WHERE k=? IF v=1"); + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifColumn("v1") + .isEqualTo(literal(1)) + .ifColumn("v2") + .isEqualTo(literal(2))) + .hasCql("DELETE FROM foo WHERE k=? IF v1=1 AND v2=2"); + } + + @Test + public void should_generate_field_condition() { + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifField("v", "f") + .isEqualTo(literal(1))) + .hasCql("DELETE FROM foo WHERE k=? IF v.f=1"); + } + + @Test + public void should_generate_element_condition() { + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifElement("v", literal(1)) + .isEqualTo(literal(1))) + .hasCql("DELETE FROM foo WHERE k=? IF v[1]=1"); + } + + @Test + public void should_generate_if_exists_condition() { + assertThat(deleteFrom("foo").whereColumn("k").isEqualTo(bindMarker()).ifExists()) + .hasCql("DELETE FROM foo WHERE k=? IF EXISTS"); + } + + @Test + public void should_cancel_if_exists_if_other_condition_added() { + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifExists() + .ifColumn("v") + .isEqualTo(literal(1))) + .hasCql("DELETE FROM foo WHERE k=? IF v=1"); + } + + @Test + public void should_cancel_other_conditions_if_if_exists_added() { + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifColumn("v1") + .isEqualTo(literal(1)) + .ifColumn("v2") + .isEqualTo(literal(2)) + .ifExists()) + .hasCql("DELETE FROM foo WHERE k=? IF EXISTS"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteFluentRelationTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteFluentRelationTest.java new file mode 100644 index 00000000000..fa8afdf5a67 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteFluentRelationTest.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.delete; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom; + +import com.datastax.oss.driver.api.querybuilder.relation.RelationTest; +import com.datastax.oss.driver.api.querybuilder.select.SelectFluentRelationTest; +import org.junit.Test; + +/** + * Mostly covered by other tests already. + * + * @see SelectFluentRelationTest + * @see RelationTest + */ +public class DeleteFluentRelationTest { + + @Test + public void should_generate_delete_with_column_relation() { + assertThat(deleteFrom("foo").whereColumn("k").isEqualTo(bindMarker())) + .hasCql("DELETE FROM foo WHERE k=?"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteIdempotenceTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteIdempotenceTest.java new file mode 100644 index 00000000000..5d9cb05a914 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteIdempotenceTest.java @@ -0,0 +1,63 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.delete; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.function; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.raw; + +import org.junit.Test; + +public class DeleteIdempotenceTest { + + @Test + public void should_not_be_idempotent_if_conditional() { + assertThat(deleteFrom("foo").whereColumn("k").isEqualTo(bindMarker())) + .hasCql("DELETE FROM foo WHERE k=?") + .isIdempotent(); + assertThat(deleteFrom("foo").whereColumn("k").isEqualTo(bindMarker()).ifExists()) + .hasCql("DELETE FROM foo WHERE k=? IF EXISTS") + .isNotIdempotent(); + assertThat( + deleteFrom("foo") + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifColumn("k") + .isEqualTo(literal(1))) + .hasCql("DELETE FROM foo WHERE k=? IF k=1") + .isNotIdempotent(); + } + + @Test + public void should_not_be_idempotent_if_deleting_collection_element() { + assertThat(deleteFrom("foo").element("l", literal(0)).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("DELETE l[0] FROM foo WHERE k=?") + .isNotIdempotent(); + } + + @Test + public void should_not_be_idempotent_if_using_non_idempotent_term_in_relation() { + assertThat(deleteFrom("foo").whereColumn("k").isEqualTo(function("non_idempotent_func"))) + .hasCql("DELETE FROM foo WHERE k=non_idempotent_func()") + .isNotIdempotent(); + assertThat(deleteFrom("foo").whereColumn("k").isEqualTo(raw("1"))) + .hasCql("DELETE FROM foo WHERE k=1") + .isNotIdempotent(); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteSelectorTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteSelectorTest.java new file mode 100644 index 00000000000..7c3f7685bb5 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteSelectorTest.java @@ -0,0 +1,47 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.delete; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; + +import org.junit.Test; + +public class DeleteSelectorTest { + + @Test + public void should_generate_column_deletion() { + assertThat(deleteFrom("foo").column("v").whereColumn("k").isEqualTo(bindMarker())) + .hasCql("DELETE v FROM foo WHERE k=?"); + assertThat(deleteFrom("ks", "foo").column("v").whereColumn("k").isEqualTo(bindMarker())) + .hasCql("DELETE v FROM ks.foo WHERE k=?"); + } + + @Test + public void should_generate_field_deletion() { + assertThat( + deleteFrom("foo").field("address", "street").whereColumn("k").isEqualTo(bindMarker())) + .hasCql("DELETE address.street FROM foo WHERE k=?"); + } + + @Test + public void should_generate_element_deletion() { + assertThat(deleteFrom("foo").element("m", literal(1)).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("DELETE m[1] FROM foo WHERE k=?"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteTimestampTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteTimestampTest.java new file mode 100644 index 00000000000..27c343694c7 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/delete/DeleteTimestampTest.java @@ -0,0 +1,60 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.delete; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom; + +import org.junit.Test; + +public class DeleteTimestampTest { + + @Test + public void should_generate_using_timestamp_clause() { + assertThat(deleteFrom("foo").usingTimestamp(1).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("DELETE FROM foo USING TIMESTAMP 1 WHERE k=?"); + assertThat( + deleteFrom("foo").usingTimestamp(bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("DELETE FROM foo USING TIMESTAMP ? WHERE k=?"); + assertThat( + deleteFrom("foo") + .column("v") + .usingTimestamp(1) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("DELETE v FROM foo USING TIMESTAMP 1 WHERE k=?"); + assertThat( + deleteFrom("foo") + .column("v") + .usingTimestamp(bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("DELETE v FROM foo USING TIMESTAMP ? WHERE k=?"); + } + + @Test + public void should_use_last_timestamp_if_called_multiple_times() { + assertThat( + deleteFrom("foo") + .usingTimestamp(1) + .usingTimestamp(2) + .usingTimestamp(3) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("DELETE FROM foo USING TIMESTAMP 3 WHERE k=?"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/insert/InsertIdempotenceTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/insert/InsertIdempotenceTest.java new file mode 100644 index 00000000000..57fd40152fa --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/insert/InsertIdempotenceTest.java @@ -0,0 +1,66 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.insert; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.add; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.function; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.insertInto; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.raw; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.tuple; + +import org.junit.Test; + +public class InsertIdempotenceTest { + + @Test + public void should_not_be_idempotent_if_conditional() { + assertThat(insertInto("foo").value("k", literal(1))) + .hasCql("INSERT INTO foo (k) VALUES (1)") + .isIdempotent(); + assertThat(insertInto("foo").value("k", literal(1)).ifNotExists()) + .hasCql("INSERT INTO foo (k) VALUES (1) IF NOT EXISTS") + .isNotIdempotent(); + } + + @Test + public void should_not_be_idempotent_if_inserting_non_idempotent_term() { + assertThat(insertInto("foo").value("k", literal(1))) + .hasCql("INSERT INTO foo (k) VALUES (1)") + .isIdempotent(); + assertThat(insertInto("foo").value("k", function("generate_id"))) + .hasCql("INSERT INTO foo (k) VALUES (generate_id())") + .isNotIdempotent(); + assertThat(insertInto("foo").value("k", raw("generate_id()"))) + .hasCql("INSERT INTO foo (k) VALUES (generate_id())") + .isNotIdempotent(); + + assertThat(insertInto("foo").value("k", add(literal(1), literal(1)))) + .hasCql("INSERT INTO foo (k) VALUES (1+1)") + .isIdempotent(); + assertThat(insertInto("foo").value("k", add(literal(1), function("generate_id")))) + .hasCql("INSERT INTO foo (k) VALUES (1+generate_id())") + .isNotIdempotent(); + + assertThat(insertInto("foo").value("k", tuple(literal(1), literal(1)))) + .hasCql("INSERT INTO foo (k) VALUES ((1,1))") + .isIdempotent(); + assertThat(insertInto("foo").value("k", tuple(literal(1), function("generate_id")))) + .hasCql("INSERT INTO foo (k) VALUES ((1,generate_id()))") + .isNotIdempotent(); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/insert/JsonInsertTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/insert/JsonInsertTest.java new file mode 100644 index 00000000000..5bfd7eea59a --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/insert/JsonInsertTest.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.insert; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.insertInto; + +import org.junit.Test; + +public class JsonInsertTest { + + @Test + public void should_generate_insert_json() { + assertThat(insertInto("foo").json("{\"bar\": 1}")) + .hasCql("INSERT INTO foo JSON '{\"bar\": 1}'"); + assertThat(insertInto("foo").json(bindMarker())).hasCql("INSERT INTO foo JSON ?"); + assertThat(insertInto("foo").json(bindMarker()).defaultNull()) + .hasCql("INSERT INTO foo JSON ? DEFAULT NULL"); + assertThat(insertInto("foo").json(bindMarker()).defaultUnset()) + .hasCql("INSERT INTO foo JSON ? DEFAULT UNSET"); + } + + @Test + public void should_keep_last_missing_json_behavior() { + assertThat(insertInto("foo").json(bindMarker()).defaultNull().defaultUnset()) + .hasCql("INSERT INTO foo JSON ? DEFAULT UNSET"); + } + + @Test + public void should_generate_if_not_exists_and_timestamp_clauses() { + assertThat(insertInto("foo").json(bindMarker()).ifNotExists().usingTimestamp(1)) + .hasCql("INSERT INTO foo JSON ? IF NOT EXISTS USING TIMESTAMP 1"); + assertThat(insertInto("foo").json(bindMarker()).defaultUnset().ifNotExists().usingTimestamp(1)) + .hasCql("INSERT INTO foo JSON ? DEFAULT UNSET IF NOT EXISTS USING TIMESTAMP 1"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/insert/RegularInsertTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/insert/RegularInsertTest.java new file mode 100644 index 00000000000..c445411b88c --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/insert/RegularInsertTest.java @@ -0,0 +1,77 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.insert; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.insertInto; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; + +import org.junit.Test; + +public class RegularInsertTest { + + @Test + public void should_generate_column_assignments() { + assertThat(insertInto("foo").value("a", literal(1)).value("b", literal(2))) + .hasCql("INSERT INTO foo (a,b) VALUES (1,2)"); + assertThat(insertInto("ks", "foo").value("a", literal(1)).value("b", literal(2))) + .hasCql("INSERT INTO ks.foo (a,b) VALUES (1,2)"); + assertThat(insertInto("foo").value("a", bindMarker()).value("b", bindMarker())) + .hasCql("INSERT INTO foo (a,b) VALUES (?,?)"); + } + + @Test + public void should_keep_last_assignment_if_column_listed_twice() { + assertThat( + insertInto("foo") + .value("a", bindMarker()) + .value("b", bindMarker()) + .value("a", literal(1))) + .hasCql("INSERT INTO foo (b,a) VALUES (?,1)"); + } + + @Test + public void should_generate_if_not_exists_clause() { + assertThat(insertInto("foo").value("a", bindMarker()).ifNotExists()) + .hasCql("INSERT INTO foo (a) VALUES (?) IF NOT EXISTS"); + } + + @Test + public void should_generate_using_timestamp_clause() { + assertThat(insertInto("foo").value("a", bindMarker()).usingTimestamp(1)) + .hasCql("INSERT INTO foo (a) VALUES (?) USING TIMESTAMP 1"); + assertThat(insertInto("foo").value("a", bindMarker()).usingTimestamp(bindMarker())) + .hasCql("INSERT INTO foo (a) VALUES (?) USING TIMESTAMP ?"); + } + + @Test + public void should_use_last_timestamp_if_called_multiple_times() { + assertThat( + insertInto("foo") + .value("a", bindMarker()) + .usingTimestamp(1) + .usingTimestamp(2) + .usingTimestamp(3)) + .hasCql("INSERT INTO foo (a) VALUES (?) USING TIMESTAMP 3"); + } + + @Test + public void should_generate_if_not_exists_and_timestamp_clauses() { + assertThat(insertInto("foo").value("a", bindMarker()).ifNotExists().usingTimestamp(1)) + .hasCql("INSERT INTO foo (a) VALUES (?) IF NOT EXISTS USING TIMESTAMP 1"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/relation/RelationTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/relation/RelationTest.java new file mode 100644 index 00000000000..f3ff81ed188 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/relation/RelationTest.java @@ -0,0 +1,139 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.relation; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.raw; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.tuple; + +import org.junit.Test; + +public class RelationTest { + + @Test + public void should_generate_comparison_relation() { + assertThat(selectFrom("foo").all().where(Relation.column("k").isEqualTo(bindMarker()))) + .hasCql("SELECT * FROM foo WHERE k=?"); + assertThat(selectFrom("foo").all().where(Relation.column("k").isEqualTo(bindMarker("value")))) + .hasCql("SELECT * FROM foo WHERE k=:value"); + } + + @Test + public void should_generate_is_not_null_relation() { + assertThat(selectFrom("foo").all().where(Relation.column("k").isNotNull())) + .hasCql("SELECT * FROM foo WHERE k IS NOT NULL"); + } + + @Test + public void should_generate_in_relation() { + assertThat(selectFrom("foo").all().where(Relation.column("k").in(bindMarker()))) + .hasCql("SELECT * FROM foo WHERE k IN ?"); + assertThat(selectFrom("foo").all().where(Relation.column("k").in(bindMarker(), bindMarker()))) + .hasCql("SELECT * FROM foo WHERE k IN (?,?)"); + } + + @Test + public void should_generate_token_relation() { + assertThat(selectFrom("foo").all().where(Relation.token("k1", "k2").isEqualTo(bindMarker("t")))) + .hasCql("SELECT * FROM foo WHERE token(k1,k2)=:t"); + } + + @Test + public void should_generate_column_component_relation() { + assertThat( + selectFrom("foo") + .all() + .where( + Relation.column("id").isEqualTo(bindMarker()), + Relation.mapValue("user", raw("'name'")).isEqualTo(bindMarker()))) + .hasCql("SELECT * FROM foo WHERE id=? AND user['name']=?"); + } + + @Test + public void should_generate_tuple_relation() { + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(bindMarker())) + .where(Relation.columns("c1", "c2", "c3").in(bindMarker()))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3) IN ?"); + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(bindMarker())) + .where(Relation.columns("c1", "c2", "c3").in(bindMarker(), bindMarker()))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3) IN (?,?)"); + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(bindMarker())) + .where(Relation.columns("c1", "c2", "c3").in(bindMarker(), raw("(4,5,6)")))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3) IN (?,(4,5,6))"); + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(bindMarker())) + .where( + Relation.columns("c1", "c2", "c3") + .in( + tuple(bindMarker(), bindMarker(), bindMarker()), + tuple(bindMarker(), bindMarker(), bindMarker())))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3) IN ((?,?,?),(?,?,?))"); + + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(bindMarker())) + .where(Relation.columns("c1", "c2", "c3").isEqualTo(bindMarker()))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3)=?"); + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(bindMarker())) + .where( + Relation.columns("c1", "c2", "c3") + .isLessThan(tuple(bindMarker(), bindMarker(), bindMarker())))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3)<(?,?,?)"); + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(bindMarker())) + .where(Relation.columns("c1", "c2", "c3").isGreaterThanOrEqualTo(raw("(1,2,3)")))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3)>=(1,2,3)"); + } + + @Test + public void should_generate_custom_index_relation() { + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(bindMarker())) + .where(Relation.customIndex("my_index", raw("'custom expression'")))) + .hasCql("SELECT * FROM foo WHERE k=? AND expr(my_index,'custom expression')"); + } + + @Test + public void should_generate_raw_relation() { + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(bindMarker())) + .where(raw("c = 'test'"))) + .hasCql("SELECT * FROM foo WHERE k=? AND c = 'test'"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/relation/TermTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/relation/TermTest.java new file mode 100644 index 00000000000..320b7c827b8 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/relation/TermTest.java @@ -0,0 +1,127 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.relation; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.add; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.currentDate; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.currentTime; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.currentTimeUuid; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.currentTimestamp; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.function; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.maxTimeUuid; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.minTimeUuid; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.multiply; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.negate; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.now; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.raw; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.remainder; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.subtract; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.toDate; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.toTimestamp; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.toUnixTimestamp; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.typeHint; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.data.TupleValue; +import com.datastax.oss.driver.api.core.data.UdtValue; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.TupleType; +import com.datastax.oss.driver.api.core.type.UserDefinedType; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.querybuilder.CharsetCodec; +import com.datastax.oss.driver.internal.core.type.UserDefinedTypeBuilder; +import com.datastax.oss.driver.shaded.guava.common.base.Charsets; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import java.util.Date; +import org.junit.Test; + +public class TermTest { + + @Test + public void should_generate_arithmetic_terms() { + assertThat(add(raw("a"), raw("b"))).hasCql("a+b"); + assertThat(add(add(raw("a"), raw("b")), add(raw("c"), raw("d")))).hasCql("a+b+c+d"); + assertThat(subtract(add(raw("a"), raw("b")), add(raw("c"), raw("d")))).hasCql("a+b-(c+d)"); + assertThat(subtract(add(raw("a"), raw("b")), subtract(raw("c"), raw("d")))).hasCql("a+b-(c-d)"); + assertThat(negate(add(raw("a"), raw("b")))).hasCql("-(a+b)"); + assertThat(negate(subtract(raw("a"), raw("b")))).hasCql("-(a-b)"); + assertThat(multiply(add(raw("a"), raw("b")), add(raw("c"), raw("d")))).hasCql("(a+b)*(c+d)"); + assertThat(remainder(multiply(raw("a"), raw("b")), multiply(raw("c"), raw("d")))) + .hasCql("a*b%(c*d)"); + assertThat(remainder(multiply(raw("a"), raw("b")), remainder(raw("c"), raw("d")))) + .hasCql("a*b%(c%d)"); + } + + @Test + public void should_generate_function_terms() { + assertThat(function("f")).hasCql("f()"); + assertThat(function("f", raw("a"), raw("b"))).hasCql("f(a,b)"); + assertThat(function("ks", "f", raw("a"), raw("b"))).hasCql("ks.f(a,b)"); + assertThat(now()).hasCql("now()"); + assertThat(currentTimestamp()).hasCql("currenttimestamp()"); + assertThat(currentDate()).hasCql("currentdate()"); + assertThat(currentTime()).hasCql("currenttime()"); + assertThat(currentTimeUuid()).hasCql("currenttimeuuid()"); + assertThat(minTimeUuid(raw("a"))).hasCql("mintimeuuid(a)"); + assertThat(maxTimeUuid(raw("a"))).hasCql("maxtimeuuid(a)"); + assertThat(toDate(raw("a"))).hasCql("todate(a)"); + assertThat(toTimestamp(raw("a"))).hasCql("totimestamp(a)"); + assertThat(toUnixTimestamp(raw("a"))).hasCql("tounixtimestamp(a)"); + } + + @Test + public void should_generate_type_hint_terms() { + assertThat(typeHint(raw("1"), DataTypes.BIGINT)).hasCql("(bigint)1"); + } + + @Test + public void should_generate_literal_terms() { + assertThat(literal(1)).hasCql("1"); + assertThat(literal("foo")).hasCql("'foo'"); + assertThat(literal(ImmutableList.of(1, 2, 3))).hasCql("[1,2,3]"); + + TupleType tupleType = DataTypes.tupleOf(DataTypes.INT, DataTypes.TEXT); + TupleValue tupleValue = tupleType.newValue().setInt(0, 1).setString(1, "foo"); + assertThat(literal(tupleValue)).hasCql("(1,'foo')"); + + UserDefinedType udtType = + new UserDefinedTypeBuilder(CqlIdentifier.fromCql("ks"), CqlIdentifier.fromCql("user")) + .withField(CqlIdentifier.fromCql("first_name"), DataTypes.TEXT) + .withField(CqlIdentifier.fromCql("last_name"), DataTypes.TEXT) + .build(); + UdtValue udtValue = + udtType.newValue().setString("first_name", "Jane").setString("last_name", "Doe"); + assertThat(literal(udtValue)).hasCql("{first_name:'Jane',last_name:'Doe'}"); + assertThat(literal(null)).hasCql("NULL"); + + assertThat(literal(Charsets.UTF_8, new CharsetCodec())).hasCql("'UTF-8'"); + assertThat(literal(Charsets.UTF_8, CharsetCodec.TEST_REGISTRY)).hasCql("'UTF-8'"); + } + + @Test + public void should_fail_when_no_codec_for_literal() { + assertThatThrownBy(() -> literal(new Date(2018, 10, 10))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Could not inline literal of type java.util.Date. " + + "This happens because the driver doesn't know how to map it to a CQL type. " + + "Try passing a TypeCodec or CodecRegistry to literal().") + .hasCauseInstanceOf(CodecNotFoundException.class); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterKeyspaceTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterKeyspaceTest.java new file mode 100644 index 00000000000..6d56be76c29 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterKeyspaceTest.java @@ -0,0 +1,42 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.alterKeyspace; + +import org.junit.Test; + +public class AlterKeyspaceTest { + + @Test + public void should_not_throw_on_toString_for_AlterKeyspaceStart() { + assertThat(alterKeyspace("foo").toString()).isEqualTo("ALTER KEYSPACE foo"); + } + + @Test + public void should_generate_alter_keyspace_with_replication() { + assertThat(alterKeyspace("foo").withSimpleStrategy(3)) + .hasCql( + "ALTER KEYSPACE foo WITH replication={'class':'SimpleStrategy','replication_factor':3}"); + } + + @Test + public void should_generate_alter_keyspace_with_durable_writes_and_options() { + assertThat(alterKeyspace("foo").withDurableWrites(true).withOption("hello", "world")) + .hasCql("ALTER KEYSPACE foo WITH durable_writes=true AND hello='world'"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterMaterializedViewTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterMaterializedViewTest.java new file mode 100644 index 00000000000..360b6f35183 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterMaterializedViewTest.java @@ -0,0 +1,43 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.alterMaterializedView; + +import org.junit.Test; + +public class AlterMaterializedViewTest { + + @Test + public void should_not_throw_on_toString_for_AlterMaterializedViewStart() { + assertThat(alterMaterializedView("foo").toString()).isEqualTo("ALTER MATERIALIZED VIEW foo"); + } + + @Test + public void should_generate_alter_view_with_options() { + assertThat( + alterMaterializedView("baz").withLZ4Compression().withDefaultTimeToLiveSeconds(86400)) + .hasCql( + "ALTER MATERIALIZED VIEW baz WITH compression={'class':'LZ4Compressor'} AND default_time_to_live=86400"); + } + + @Test + public void should_generate_alter_view_with_keyspace_options() { + assertThat(alterMaterializedView("foo", "baz").withCDC(true)) + .hasCql("ALTER MATERIALIZED VIEW foo.baz WITH cdc=true"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterTableTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterTableTest.java new file mode 100644 index 00000000000..16db985ba9c --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterTableTest.java @@ -0,0 +1,103 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.alterTable; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import org.junit.Test; + +public class AlterTableTest { + + @Test + public void should_not_throw_on_toString_for_AlterTableStart() { + assertThat(alterTable("foo").toString()).isEqualTo("ALTER TABLE foo"); + } + + @Test + public void should_generate_alter_table_with_alter_column_type() { + assertThat(alterTable("foo", "bar").alterColumn("x", DataTypes.TEXT)) + .hasCql("ALTER TABLE foo.bar ALTER x TYPE text"); + } + + @Test + public void should_generate_alter_table_with_add_single_column() { + assertThat(alterTable("foo", "bar").addColumn("x", DataTypes.TEXT)) + .hasCql("ALTER TABLE foo.bar ADD x text"); + } + + @Test + public void should_generate_alter_table_with_add_three_columns() { + assertThat( + alterTable("foo", "bar") + .addColumn("x", DataTypes.TEXT) + .addStaticColumn("y", DataTypes.FLOAT) + .addColumn("z", DataTypes.DOUBLE)) + .hasCql("ALTER TABLE foo.bar ADD (x text,y float STATIC,z double)"); + } + + @Test + public void should_generate_alter_table_with_drop_single_column() { + assertThat(alterTable("foo", "bar").dropColumn("x")).hasCql("ALTER TABLE foo.bar DROP x"); + } + + @Test + public void should_generate_alter_table_with_drop_two_columns() { + assertThat(alterTable("foo", "bar").dropColumn("x").dropColumn("y")) + .hasCql("ALTER TABLE foo.bar DROP (x,y)"); + } + + @Test + public void should_generate_alter_table_with_drop_two_columns_at_once() { + assertThat(alterTable("foo", "bar").dropColumns("x", "y")) + .hasCql("ALTER TABLE foo.bar DROP (x,y)"); + } + + @Test + public void should_generate_alter_table_with_rename_single_column() { + assertThat(alterTable("foo", "bar").renameColumn("x", "y")) + .hasCql("ALTER TABLE foo.bar RENAME x TO y"); + } + + @Test + public void should_generate_alter_table_with_rename_three_columns() { + assertThat( + alterTable("foo", "bar") + .renameColumn("x", "y") + .renameColumn("u", "v") + .renameColumn("b", "a")) + .hasCql("ALTER TABLE foo.bar RENAME x TO y AND u TO v AND b TO a"); + } + + @Test + public void should_generate_alter_table_with_drop_compact_storage() { + assertThat(alterTable("bar").dropCompactStorage()) + .hasCql("ALTER TABLE bar DROP COMPACT STORAGE"); + } + + @Test + public void should_generate_alter_table_with_options() { + assertThat(alterTable("bar").withComment("Hello").withCDC(true)) + .hasCql("ALTER TABLE bar WITH comment='Hello' AND cdc=true"); + } + + @Test + public void should_generate_alter_table_with_no_compression() { + assertThat(alterTable("bar").withNoCompression()) + .hasCql("ALTER TABLE bar WITH compression={'sstable_compression':''}"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterTypeTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterTypeTest.java new file mode 100644 index 00000000000..6ae49c8533c --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/AlterTypeTest.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.alterType; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import org.junit.Test; + +public class AlterTypeTest { + + @Test + public void should_not_throw_on_toString_for_AlterTypeStart() { + assertThat(alterType("foo").toString()).isEqualTo("ALTER TYPE foo"); + } + + @Test + public void should_generate_alter_type_with_alter_field_type() { + assertThat(alterType("foo", "bar").alterField("x", DataTypes.TEXT)) + .hasCql("ALTER TYPE foo.bar ALTER x TYPE text"); + } + + @Test + public void should_generate_alter_table_with_add_field() { + assertThat(alterType("foo", "bar").addField("x", DataTypes.TEXT)) + .hasCql("ALTER TYPE foo.bar ADD x text"); + } + + @Test + public void should_generate_alter_table_with_rename_single_column() { + assertThat(alterType("foo", "bar").renameField("x", "y")) + .hasCql("ALTER TYPE foo.bar RENAME x TO y"); + } + + @Test + public void should_generate_alter_table_with_rename_three_columns() { + assertThat(alterType("bar").renameField("x", "y").renameField("u", "v").renameField("b", "a")) + .hasCql("ALTER TYPE bar RENAME x TO y AND u TO v AND b TO a"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateAggregateTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateAggregateTest.java new file mode 100644 index 00000000000..f9dcf41d41a --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateAggregateTest.java @@ -0,0 +1,151 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.tuple; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.createAggregate; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import org.junit.Test; + +public class CreateAggregateTest { + @Test + public void should_create_aggreate_with_simple_param() { + + assertThat( + createAggregate("keyspace1", "agg1") + .withParameter(DataTypes.INT) + .withSFunc("sfunction") + .withSType(DataTypes.ASCII) + .withFinalFunc("finalfunction") + .withInitCond(tuple(literal(0), literal(0)))) + .hasCql( + "CREATE AGGREGATE keyspace1.agg1 (int) SFUNC sfunction STYPE ascii FINALFUNC finalfunction INITCOND (0,0)"); + } + + @Test + public void should_create_aggregate_with_many_params() { + + assertThat( + createAggregate("keyspace1", "agg2") + .withParameter(DataTypes.INT) + .withParameter(DataTypes.TEXT) + .withParameter(DataTypes.BOOLEAN) + .withSFunc("sfunction") + .withSType(DataTypes.ASCII) + .withFinalFunc("finalfunction") + .withInitCond(tuple(literal(0), literal(0)))) + .hasCql( + "CREATE AGGREGATE keyspace1.agg2 (int,text,boolean) SFUNC sfunction STYPE ascii FINALFUNC finalfunction INITCOND (0,0)"); + } + + @Test + public void should_create_aggregate_with_param_without_frozen() { + + assertThat( + createAggregate("keyspace1", "agg9") + .withParameter(DataTypes.tupleOf(DataTypes.TEXT)) + .withSFunc("sfunction") + .withSType(DataTypes.ASCII) + .withFinalFunc("finalfunction") + .withInitCond(tuple(literal(0), literal(0)))) + .hasCql( + "CREATE AGGREGATE keyspace1.agg9 (tuple) SFUNC sfunction STYPE ascii FINALFUNC finalfunction INITCOND (0,0)"); + } + + @Test + public void should_create_aggregate_with_no_params() { + + assertThat( + createAggregate("keyspace1", "agg3") + .withSFunc("sfunction") + .withSType(DataTypes.ASCII) + .withFinalFunc("finalfunction") + .withInitCond(tuple(literal(0), literal(0)))) + .hasCql( + "CREATE AGGREGATE keyspace1.agg3 () SFUNC sfunction STYPE ascii FINALFUNC finalfunction INITCOND (0,0)"); + } + + @Test + public void should_create_aggregate_with_no_keyspace() { + + assertThat( + createAggregate("agg4") + .withSFunc("sfunction") + .withSType(DataTypes.ASCII) + .withFinalFunc("finalfunction") + .withInitCond(tuple(literal(0), literal(0)))) + .hasCql( + "CREATE AGGREGATE agg4 () SFUNC sfunction STYPE ascii FINALFUNC finalfunction INITCOND (0,0)"); + } + + @Test + public void should_create_aggregate_with_if_not_exists() { + + assertThat( + createAggregate("agg6") + .ifNotExists() + .withSFunc("sfunction") + .withSType(DataTypes.ASCII) + .withFinalFunc("finalfunction") + .withInitCond(tuple(literal(0), literal(0)))) + .hasCql( + "CREATE AGGREGATE IF NOT EXISTS agg6 () SFUNC sfunction STYPE ascii FINALFUNC finalfunction INITCOND (0,0)"); + } + + @Test + public void should_create_aggregate_with_no_final_func() { + + assertThat( + createAggregate("cycling", "sum") + .withParameter(DataTypes.INT) + .withSFunc("dsum") + .withSType(DataTypes.INT)) + .hasCql("CREATE AGGREGATE cycling.sum (int) SFUNC dsum STYPE int"); + } + + @Test + public void should_create_or_replace() { + assertThat( + createAggregate("keyspace1", "agg7") + .orReplace() + .withSFunc("sfunction") + .withSType(DataTypes.ASCII) + .withFinalFunc("finalfunction") + .withInitCond(tuple(literal(0), literal(0)))) + .hasCql( + "CREATE OR REPLACE AGGREGATE keyspace1.agg7 () SFUNC sfunction STYPE ascii FINALFUNC finalfunction INITCOND (0,0)"); + } + + @Test + public void should_not_throw_on_toString_for_CreateAggregateStart() { + assertThat(createAggregate("agg1").toString()).isEqualTo("CREATE AGGREGATE agg1 ()"); + } + + @Test + public void should_not_throw_on_toString_for_CreateAggregateWithParam() { + assertThat(createAggregate("func1").withParameter(DataTypes.INT).toString()) + .isEqualTo("CREATE AGGREGATE func1 (int)"); + } + + @Test + public void should_not_throw_on_toString_for_NotExists_OrReplace() { + assertThat(createAggregate("func1").ifNotExists().orReplace().toString()) + .isEqualTo("CREATE OR REPLACE AGGREGATE IF NOT EXISTS func1 ()"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateFunctionTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateFunctionTest.java new file mode 100644 index 00000000000..02a91fd627d --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateFunctionTest.java @@ -0,0 +1,188 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.createFunction; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.udt; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import org.junit.Test; + +public class CreateFunctionTest { + + @Test + public void should_not_throw_on_toString_for_CreateFunctionStart() { + assertThat(createFunction("func1").toString()) + .isEqualTo("CREATE FUNCTION func1 () CALLED ON NULL INPUT"); + } + + @Test + public void should_not_throw_on_toString_for_CreateFunctionWithType() { + assertThat( + createFunction("func1") + .withParameter("param1", DataTypes.INT) + .returnsNullOnNull() + .returnsType(DataTypes.INT) + .toString()) + .isEqualTo("CREATE FUNCTION func1 (param1 int) RETURNS NULL ON NULL INPUT RETURNS int"); + } + + @Test + public void should_not_throw_on_toString_for_CreateFunctionWithLanguage() { + assertThat( + createFunction("func1") + .withParameter("param1", DataTypes.INT) + .returnsNullOnNull() + .returnsType(DataTypes.INT) + .withJavaLanguage() + .toString()) + .isEqualTo( + "CREATE FUNCTION func1 (param1 int) RETURNS NULL ON NULL INPUT RETURNS int LANGUAGE java"); + } + + @Test + public void should_create_function_with_simple_params() { + assertThat( + createFunction("keyspace1", "func1") + .withParameter("param1", DataTypes.INT) + .calledOnNull() + .returnsType(DataTypes.TEXT) + .withJavaLanguage() + .asQuoted("return Integer.toString(param1);")) + .hasCql( + "CREATE FUNCTION keyspace1.func1 (param1 int) CALLED ON NULL INPUT RETURNS text LANGUAGE java AS 'return Integer.toString(param1);'"); + } + + @Test + public void should_create_function_with_param_and_return_type_not_frozen() { + assertThat( + createFunction("keyspace1", "func6") + .withParameter("param1", DataTypes.tupleOf(DataTypes.INT, DataTypes.INT)) + .returnsNullOnNull() + .returnsType(udt("person", true)) + .withJavaLanguage() + .as("'return Integer.toString(param1);'")) + .hasCql( + "CREATE FUNCTION keyspace1.func6 (param1 tuple) RETURNS NULL ON NULL INPUT RETURNS person LANGUAGE java AS 'return Integer.toString(param1);'"); + } + + @Test + public void should_honor_returns_null() { + assertThat( + createFunction("keyspace1", "func2") + .withParameter("param1", DataTypes.INT) + .returnsNullOnNull() + .returnsType(DataTypes.TEXT) + .withJavaLanguage() + .asQuoted("return Integer.toString(param1);")) + .hasCql( + "CREATE FUNCTION keyspace1.func2 (param1 int) RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE java AS 'return Integer.toString(param1);'"); + } + + @Test + public void should_create_function_with_many_params() { + assertThat( + createFunction("keyspace1", "func3") + .withParameter("param1", DataTypes.INT) + .withParameter("param2", DataTypes.TEXT) + .withParameter("param3", DataTypes.BOOLEAN) + .returnsNullOnNull() + .returnsType(DataTypes.TEXT) + .withJavaLanguage() + .asQuoted("return Integer.toString(param1);")) + .hasCql( + "CREATE FUNCTION keyspace1.func3 (param1 int,param2 text,param3 boolean) RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE java AS 'return Integer.toString(param1);'"); + } + + @Test + public void should_create_function_with_no_params() { + + assertThat( + createFunction("keyspace1", "func4") + .returnsNullOnNull() + .returnsType(DataTypes.TEXT) + .withLanguage("java") + .asQuoted("return \"hello world\";")) + .hasCql( + "CREATE FUNCTION keyspace1.func4 () RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE java AS 'return \"hello world\";'"); + } + + @Test + public void should_create_function_with_no_keyspace() { + assertThat( + createFunction("func5") + .returnsNullOnNull() + .returnsType(DataTypes.TEXT) + .withJavaLanguage() + .asQuoted("return \"hello world\";")) + .hasCql( + "CREATE FUNCTION func5 () RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE java AS 'return \"hello world\";'"); + } + + @Test + public void should_create_function_with_if_not_exists() { + assertThat( + createFunction("keyspace1", "func6") + .ifNotExists() + .returnsNullOnNull() + .returnsType(DataTypes.TEXT) + .withJavaLanguage() + .asQuoted("return \"hello world\";")) + .hasCql( + "CREATE FUNCTION IF NOT EXISTS keyspace1.func6 () RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE java AS 'return \"hello world\";'"); + } + + @Test + public void should_create_or_replace() { + assertThat( + createFunction("keyspace1", "func6") + .orReplace() + .withParameter("param1", DataTypes.INT) + .returnsNullOnNull() + .returnsType(DataTypes.TEXT) + .withJavaLanguage() + .asQuoted("return Integer.toString(param1);")) + .hasCql( + "CREATE OR REPLACE FUNCTION keyspace1.func6 (param1 int) RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE java AS 'return Integer.toString(param1);'"); + } + + @Test + public void should_not_quote_body_using_as() { + assertThat( + createFunction("keyspace1", "func6") + .withParameter("param1", DataTypes.INT) + .returnsNullOnNull() + .returnsType(DataTypes.TEXT) + .withJavaLanguage() + .as("'return Integer.toString(param1);'")) + .hasCql( + "CREATE FUNCTION keyspace1.func6 (param1 int) RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE java AS 'return Integer.toString(param1);'"); + } + + @Test + public void should_quote_with_dollar_signs_on_asQuoted_if_body_contains_single_quote() { + assertThat( + createFunction("keyspace1", "func6") + .withParameter("param1", DataTypes.INT) + .returnsNullOnNull() + .returnsType(DataTypes.TEXT) + .withJavaScriptLanguage() + .asQuoted("'hello ' + param1;")) + .hasCql( + "CREATE FUNCTION keyspace1.func6 (param1 int) RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE javascript AS $$ 'hello ' + param1; $$"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateIndexTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateIndexTest.java new file mode 100644 index 00000000000..d654219f23c --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateIndexTest.java @@ -0,0 +1,145 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.createIndex; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import org.junit.Test; + +public class CreateIndexTest { + + @Test + public void should_not_throw_on_toString_for_CreateIndexStart() { + assertThat(createIndex().toString()).isEqualTo("CREATE INDEX"); + } + + @Test + public void should_not_throw_on_toString_for_CreateIndexOnTable() { + assertThat(createIndex().onTable("x").toString()).isEqualTo("CREATE INDEX ON x"); + } + + @Test + public void should_generate_create_index_with_no_name() { + assertThat(createIndex().onTable("x").andColumn("y")).hasCql("CREATE INDEX ON x (y)"); + } + + @Test + public void should_generate_create_custom_index_with_no_name() { + assertThat(createIndex().custom("MyClass").onTable("x").andColumn("y")) + .hasCql("CREATE CUSTOM INDEX ON x (y) USING 'MyClass'"); + } + + @Test + public void should_generate_create_custom_index_if_not_exists_with_no_name() { + assertThat(createIndex().custom("MyClass").ifNotExists().onTable("x").andColumn("y")) + .hasCql("CREATE CUSTOM INDEX IF NOT EXISTS ON x (y) USING 'MyClass'"); + } + + @Test + public void should_generate_create_index_with_no_name_if_not_exists() { + assertThat(createIndex().ifNotExists().onTable("x").andColumn("y")) + .hasCql("CREATE INDEX IF NOT EXISTS ON x (y)"); + } + + @Test + public void should_generate_custom_index_with_name() { + assertThat(createIndex("bar").custom("MyClass").onTable("x").andColumn("y")) + .hasCql("CREATE CUSTOM INDEX bar ON x (y) USING 'MyClass'"); + } + + @Test + public void should_generate_create_custom_index_if_not_exists_with_name() { + assertThat(createIndex("bar").custom("MyClass").ifNotExists().onTable("x").andColumn("y")) + .hasCql("CREATE CUSTOM INDEX IF NOT EXISTS bar ON x (y) USING 'MyClass'"); + } + + @Test + public void should_generate_index_with_keyspace() { + assertThat(createIndex("bar").onTable("foo", "x").andColumn("y")) + .hasCql("CREATE INDEX bar ON foo.x (y)"); + } + + @Test + public void should_generate_create_index_with_name_if_not_exists() { + assertThat(createIndex("bar").ifNotExists().onTable("x").andColumn("y")) + .hasCql("CREATE INDEX IF NOT EXISTS bar ON x (y)"); + } + + @Test + public void should_generate_create_index_values() { + assertThat(createIndex().onTable("x").andColumnValues("m")) + .hasCql("CREATE INDEX ON x (VALUES(m))"); + } + + @Test + public void should_generate_create_index_keys() { + assertThat(createIndex().onTable("x").andColumnKeys("m")).hasCql("CREATE INDEX ON x (KEYS(m))"); + } + + @Test + public void should_generate_create_index_entries() { + assertThat(createIndex().onTable("x").andColumnEntries("m")) + .hasCql("CREATE INDEX ON x (ENTRIES(m))"); + } + + @Test + public void should_generate_create_index_full() { + assertThat(createIndex().onTable("x").andColumnFull("l")).hasCql("CREATE INDEX ON x (FULL(l))"); + } + + @Test + public void should_generate_create_index_custom_index_type() { + assertThat(createIndex().onTable("x").andColumn("m", "CUST")) + .hasCql("CREATE INDEX ON x (CUST(m))"); + } + + @Test + public void should_generate_create_index_with_options() { + assertThat( + createIndex() + .custom("MyClass") + .onTable("x") + .andColumn("y") + .withOption("opt1", 1) + .withOption("opt2", "data")) + .hasCql("CREATE CUSTOM INDEX ON x (y) USING 'MyClass' WITH opt1=1 AND opt2='data'"); + } + + @Test + public void should_generate_create_custom_index_with_options() { + assertThat( + createIndex() + .onTable("x") + .andColumn("y") + .withOption("opt1", 1) + .withOption("opt2", "data")) + .hasCql("CREATE INDEX ON x (y) WITH opt1=1 AND opt2='data'"); + } + + @Test + public void should_generate_create_index_sasi_with_options() { + assertThat( + createIndex() + .usingSASI() + .onTable("x") + .andColumn("y") + .withSASIOptions(ImmutableMap.of("mode", "CONTAINS", "tokenization_locale", "en"))) + .hasCql( + "CREATE CUSTOM INDEX ON x (y) USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS={'mode':'CONTAINS','tokenization_locale':'en'}"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateKeyspaceTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateKeyspaceTest.java new file mode 100644 index 00000000000..3c067aa9e3c --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateKeyspaceTest.java @@ -0,0 +1,71 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.createKeyspace; + +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import org.junit.Test; + +public class CreateKeyspaceTest { + + @Test + public void should_not_throw_on_toString_for_CreateKeyspaceStart() { + assertThat(createKeyspace("foo").toString()).isEqualTo("CREATE KEYSPACE foo"); + } + + @Test + public void should_generate_create_keyspace_simple_strategy() { + assertThat(createKeyspace("foo").withSimpleStrategy(5)) + .hasCql( + "CREATE KEYSPACE foo WITH replication={'class':'SimpleStrategy','replication_factor':5}"); + } + + @Test + public void should_generate_create_keyspace_simple_strategy_and_durable_writes() { + assertThat(createKeyspace("foo").withSimpleStrategy(5).withDurableWrites(true)) + .hasCql( + "CREATE KEYSPACE foo WITH replication={'class':'SimpleStrategy','replication_factor':5} AND durable_writes=true"); + } + + @Test + public void should_generate_create_keyspace_if_not_exists() { + assertThat(createKeyspace("foo").ifNotExists().withSimpleStrategy(2)) + .hasCql( + "CREATE KEYSPACE IF NOT EXISTS foo WITH replication={'class':'SimpleStrategy','replication_factor':2}"); + } + + @Test + public void should_generate_create_keyspace_network_topology_strategy() { + assertThat( + createKeyspace("foo").withNetworkTopologyStrategy(ImmutableMap.of("dc1", 3, "dc2", 4))) + .hasCql( + "CREATE KEYSPACE foo WITH replication={'class':'NetworkTopologyStrategy','dc1':3,'dc2':4}"); + } + + @Test + public void should_generate_create_keyspace_with_custom_properties() { + assertThat( + createKeyspace("foo") + .withSimpleStrategy(3) + .withOption("awesome_feature", true) + .withOption("wow_factor", 11) + .withOption("random_string", "hi")) + .hasCql( + "CREATE KEYSPACE foo WITH replication={'class':'SimpleStrategy','replication_factor':3} AND awesome_feature=true AND wow_factor=11 AND random_string='hi'"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateMaterializedViewTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateMaterializedViewTest.java new file mode 100644 index 00000000000..88ff1dafd8f --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateMaterializedViewTest.java @@ -0,0 +1,139 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.createMaterializedView; + +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import org.junit.Test; + +public class CreateMaterializedViewTest { + + @Test + public void should_not_throw_on_toString_for_CreateMaterializedViewStart() { + assertThat(createMaterializedView("foo").toString()).isEqualTo("CREATE MATERIALIZED VIEW foo"); + } + + @Test + public void should_not_throw_on_toString_for_CreateMaterializedViewSelection() { + assertThat(createMaterializedView("foo").asSelectFrom("bar").toString()) + .isEqualTo("CREATE MATERIALIZED VIEW foo"); + } + + @Test + public void should_not_throw_on_toString_for_CreateMaterializedViewWhereStart() { + assertThat(createMaterializedView("foo").asSelectFrom("bar").all().toString()) + .isEqualTo("CREATE MATERIALIZED VIEW foo AS SELECT * FROM bar"); + } + + @Test + public void should_generate_create_view_if_not_exists_with_select_all() { + assertThat( + createMaterializedView("baz") + .ifNotExists() + .asSelectFrom("foo", "bar") + .all() + .whereColumn("x") + .isNotNull() + .withPartitionKey("x")) + .hasCql( + "CREATE MATERIALIZED VIEW IF NOT EXISTS baz AS SELECT * FROM foo.bar WHERE x IS NOT NULL PRIMARY KEY(x)"); + } + + @Test + public void should_generate_create_view_with_select_columns() { + assertThat( + createMaterializedView("baz") + .asSelectFrom("bar") + .columns("x", "y") + .whereColumn("x") + .isNotNull() + .whereColumn("y") + .isLessThan(literal(5)) + .withPartitionKey("x")) + .hasCql( + "CREATE MATERIALIZED VIEW baz AS SELECT x,y FROM bar WHERE x IS NOT NULL AND y<5 PRIMARY KEY(x)"); + } + + @Test + public void should_generate_create_view_with_compound_partition_key_and_clustering_columns() { + assertThat( + createMaterializedView("baz") + .asSelectFrom("bar") + .all() + .whereColumn("x") + .isNotNull() + .whereColumn("y") + .isNotNull() + .withPartitionKey("x") + .withPartitionKey("y") + .withClusteringColumn("a") + .withClusteringColumn("b")) + .hasCql( + "CREATE MATERIALIZED VIEW baz AS SELECT * FROM bar WHERE x IS NOT NULL AND y IS NOT NULL PRIMARY KEY((x,y),a,b)"); + } + + @Test + public void should_generate_create_view_with_clustering_single() { + assertThat( + createMaterializedView("baz") + .asSelectFrom("bar") + .all() + .whereColumn("x") + .isNotNull() + .withPartitionKey("x") + .withClusteringColumn("a") + .withClusteringOrder("a", ClusteringOrder.DESC)) + .hasCql( + "CREATE MATERIALIZED VIEW baz AS SELECT * FROM bar WHERE x IS NOT NULL PRIMARY KEY(x,a) WITH CLUSTERING ORDER BY (a DESC)"); + } + + @Test + public void should_generate_create_view_with_clustering_and_options() { + assertThat( + createMaterializedView("baz") + .asSelectFrom("bar") + .all() + .whereColumn("x") + .isNotNull() + .withPartitionKey("x") + .withClusteringColumn("a") + .withClusteringColumn("b") + .withClusteringOrder("a", ClusteringOrder.DESC) + .withClusteringOrder("b", ClusteringOrder.ASC) + .withCDC(true) + .withComment("Hello")) + .hasCql( + "CREATE MATERIALIZED VIEW baz AS SELECT * FROM bar WHERE x IS NOT NULL PRIMARY KEY(x,a,b) WITH CLUSTERING ORDER BY (a DESC,b ASC) AND cdc=true AND comment='Hello'"); + } + + @Test + public void should_generate_create_view_with_options() { + assertThat( + createMaterializedView("baz") + .asSelectFrom("bar") + .all() + .whereColumn("x") + .isNotNull() + .withPartitionKey("x") + .withCDC(true) + .withComment("Hello")) + .hasCql( + "CREATE MATERIALIZED VIEW baz AS SELECT * FROM bar WHERE x IS NOT NULL PRIMARY KEY(x) WITH cdc=true AND comment='Hello'"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateTableTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateTableTest.java new file mode 100644 index 00000000000..16f4c2e0d10 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateTableTest.java @@ -0,0 +1,308 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.createTable; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.udt; + +import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.querybuilder.SchemaBuilder; +import com.datastax.oss.driver.api.querybuilder.SchemaBuilder.RowsPerPartition; +import com.datastax.oss.driver.api.querybuilder.schema.compaction.TimeWindowCompactionStrategy.CompactionWindowUnit; +import com.datastax.oss.driver.api.querybuilder.schema.compaction.TimeWindowCompactionStrategy.TimestampResolution; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import org.junit.Test; + +public class CreateTableTest { + + @Test + public void should_not_throw_on_toString_for_CreateTableStart() { + assertThat(createTable("foo").toString()).isEqualTo("CREATE TABLE foo"); + } + + @Test + public void should_generate_create_table_if_not_exists() { + assertThat(createTable("bar").ifNotExists().withPartitionKey("k", DataTypes.INT)) + .hasCql("CREATE TABLE IF NOT EXISTS bar (k int PRIMARY KEY)"); + } + + @Test + public void should_generate_create_table_with_single_partition_key() { + assertThat( + createTable("bar").withPartitionKey("k", DataTypes.INT).withColumn("v", DataTypes.TEXT)) + .hasCql("CREATE TABLE bar (k int PRIMARY KEY,v text)"); + } + + @Test + public void should_generate_create_table_with_compound_partition_key() { + assertThat( + createTable("bar") + .withPartitionKey("kc", DataTypes.INT) + .withPartitionKey("ka", DataTypes.TIMESTAMP) + .withColumn("v", DataTypes.TEXT)) + .hasCql("CREATE TABLE bar (kc int,ka timestamp,v text,PRIMARY KEY((kc,ka)))"); + } + + @Test + public void should_generate_create_table_with_single_partition_key_and_clustering_column() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withClusteringColumn("c", DataTypes.TEXT) + .withColumn("v", udt("val", true))) + .hasCql("CREATE TABLE bar (k int,c text,v frozen,PRIMARY KEY(k,c))"); + } + + @Test + public void should_generate_create_table_with_static_column() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withClusteringColumn("c", DataTypes.TEXT) + .withStaticColumn("s", DataTypes.TIMEUUID) + .withColumn("v", udt("val", true))) + .hasCql("CREATE TABLE bar (k int,c text,s timeuuid STATIC,v frozen,PRIMARY KEY(k,c))"); + } + + @Test + public void should_generate_create_table_with_compound_partition_key_and_clustering_columns() { + assertThat( + createTable("bar") + .withPartitionKey("kc", DataTypes.INT) + .withPartitionKey("ka", DataTypes.TIMESTAMP) + .withClusteringColumn("c", DataTypes.FLOAT) + .withClusteringColumn("a", DataTypes.UUID) + .withColumn("v", DataTypes.TEXT)) + .hasCql( + "CREATE TABLE bar (kc int,ka timestamp,c float,a uuid,v text,PRIMARY KEY((kc,ka),c,a))"); + } + + @Test + public void should_generate_create_table_with_compact_storage() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withCompactStorage()) + .hasCql("CREATE TABLE bar (k int PRIMARY KEY,v text) WITH COMPACT STORAGE"); + } + + @Test + public void should_generate_create_table_with_clustering_single() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withClusteringColumn("c", DataTypes.TEXT) + .withColumn("v", DataTypes.TEXT) + .withClusteringOrder("c", ClusteringOrder.ASC)) + .hasCql( + "CREATE TABLE bar (k int,c text,v text,PRIMARY KEY(k,c)) WITH CLUSTERING ORDER BY (c ASC)"); + } + + @Test + public void should_generate_create_table_with_clustering_three() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withClusteringColumn("c0", DataTypes.TEXT) + .withClusteringColumn("c1", DataTypes.TEXT) + .withClusteringColumn("c2", DataTypes.TEXT) + .withColumn("v", DataTypes.TEXT) + .withClusteringOrder("c0", ClusteringOrder.DESC) + .withClusteringOrder( + ImmutableMap.of("c1", ClusteringOrder.ASC, "c2", ClusteringOrder.DESC))) + .hasCql( + "CREATE TABLE bar (k int,c0 text,c1 text,c2 text,v text,PRIMARY KEY(k,c0,c1,c2)) WITH CLUSTERING ORDER BY (c0 DESC,c1 ASC,c2 DESC)"); + } + + @Test + public void should_generate_create_table_with_compact_storage_and_default_ttl() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withCompactStorage() + .withDefaultTimeToLiveSeconds(86400)) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH COMPACT STORAGE AND default_time_to_live=86400"); + } + + @Test + public void should_generate_create_table_with_clustering_compact_storage_and_default_ttl() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withClusteringColumn("c", DataTypes.TEXT) + .withColumn("v", DataTypes.TEXT) + .withCompactStorage() + .withClusteringOrder("c", ClusteringOrder.DESC) + .withDefaultTimeToLiveSeconds(86400)) + .hasCql( + "CREATE TABLE bar (k int,c text,v text,PRIMARY KEY(k,c)) WITH COMPACT STORAGE AND CLUSTERING ORDER BY (c DESC) AND default_time_to_live=86400"); + } + + @Test + public void should_generate_create_table_with_options() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withBloomFilterFpChance(0.42) + .withCDC(false) + .withComment("Hello world") + .withDcLocalReadRepairChance(0.54) + .withDefaultTimeToLiveSeconds(86400) + .withGcGraceSeconds(864000) + .withMemtableFlushPeriodInMs(10000) + .withMinIndexInterval(1024) + .withMaxIndexInterval(4096) + .withReadRepairChance(0.55) + .withSpeculativeRetry("99percentile")) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH bloom_filter_fp_chance=0.42 AND cdc=false AND comment='Hello world' AND dclocal_read_repair_chance=0.54 AND default_time_to_live=86400 AND gc_grace_seconds=864000 AND memtable_flush_period_in_ms=10000 AND min_index_interval=1024 AND max_index_interval=4096 AND read_repair_chance=0.55 AND speculative_retry='99percentile'"); + } + + @Test + public void should_generate_create_table_lz4_compression() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withLZ4Compression()) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH compression={'class':'LZ4Compressor'}"); + } + + @Test + public void should_generate_create_table_lz4_compression_options() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withLZ4Compression(1024, .5)) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH compression={'class':'LZ4Compressor','chunk_length_kb':1024,'crc_check_chance':0.5}"); + } + + @Test + public void should_generate_create_table_snappy_compression() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withSnappyCompression()) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH compression={'class':'SnappyCompressor'}"); + } + + @Test + public void should_generate_create_table_snappy_compression_options() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withSnappyCompression(2048, .25)) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH compression={'class':'SnappyCompressor','chunk_length_kb':2048,'crc_check_chance':0.25}"); + } + + @Test + public void should_generate_create_table_deflate_compression() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withDeflateCompression()) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH compression={'class':'DeflateCompressor'}"); + } + + @Test + public void should_generate_create_table_deflate_compression_options() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withDeflateCompression(4096, .1)) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH compression={'class':'DeflateCompressor','chunk_length_kb':4096,'crc_check_chance':0.1}"); + } + + @Test + public void should_generate_create_table_caching_options() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withCaching(true, RowsPerPartition.rows(10))) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH caching={'keys':'ALL','rows_per_partition':'10'}"); + } + + @Test + public void should_generate_create_table_size_tiered_compaction() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withCompaction( + SchemaBuilder.sizeTieredCompactionStrategy() + .withBucketHigh(1.6) + .withBucketLow(0.6) + .withColdReadsToOmit(0.1) + .withMaxThreshold(33) + .withMinThreshold(5) + .withMinSSTableSizeInBytes(50000) + .withOnlyPurgeRepairedTombstones(true) + .withEnabled(false) + .withTombstoneCompactionIntervalInSeconds(86400) + .withTombstoneThreshold(0.22) + .withUncheckedTombstoneCompaction(true))) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH compaction={'class':'SizeTieredCompactionStrategy','bucket_high':1.6,'bucket_low':0.6,'cold_reads_to_omit':0.1,'max_threshold':33,'min_threshold':5,'min_sstable_size':50000,'only_purge_repaired_tombstones':true,'enabled':false,'tombstone_compaction_interval':86400,'tombstone_threshold':0.22,'unchecked_tombstone_compaction':true}"); + } + + @Test + public void should_generate_create_table_leveled_compaction() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withCompaction( + SchemaBuilder.leveledCompactionStrategy() + .withSSTableSizeInMB(110) + .withTombstoneCompactionIntervalInSeconds(3600))) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH compaction={'class':'LeveledCompactionStrategy','sstable_size_in_mb':110,'tombstone_compaction_interval':3600}"); + } + + @Test + public void should_generate_create_table_time_window_compaction() { + assertThat( + createTable("bar") + .withPartitionKey("k", DataTypes.INT) + .withColumn("v", DataTypes.TEXT) + .withCompaction( + SchemaBuilder.timeWindowCompactionStrategy() + .withCompactionWindow(10, CompactionWindowUnit.DAYS) + .withTimestampResolution(TimestampResolution.MICROSECONDS) + .withUnsafeAggressiveSSTableExpiration(false))) + .hasCql( + "CREATE TABLE bar (k int PRIMARY KEY,v text) WITH compaction={'class':'TimeWindowCompactionStrategy','compaction_window_size':10,'compaction_window_unit':'DAYS','timestamp_resolution':'MICROSECONDS','unsafe_aggressive_sstable_expiration':false}"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateTypeTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateTypeTest.java new file mode 100644 index 00000000000..7015d49067c --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/CreateTypeTest.java @@ -0,0 +1,84 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.createType; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.udt; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import org.junit.Test; + +public class CreateTypeTest { + + @Test + public void should_not_throw_on_toString_for_CreateTypeStart() { + assertThat(createType("foo").toString()).isEqualTo("CREATE TYPE foo"); + } + + @Test + public void should_create_type_with_single_field() { + + assertThat(createType("keyspace1", "type").withField("single", DataTypes.TEXT)) + .hasCql("CREATE TYPE keyspace1.type (single text)"); + assertThat(createType("type").withField("single", DataTypes.TEXT)) + .hasCql("CREATE TYPE type (single text)"); + + assertThat(createType("type").ifNotExists().withField("single", DataTypes.TEXT)) + .hasCql("CREATE TYPE IF NOT EXISTS type (single text)"); + } + + @Test + public void should_create_type_with_many_fields() { + + assertThat( + createType("keyspace1", "type") + .withField("first", DataTypes.TEXT) + .withField("second", DataTypes.INT) + .withField("third", DataTypes.BLOB) + .withField("fourth", DataTypes.BOOLEAN)) + .hasCql("CREATE TYPE keyspace1.type (first text,second int,third blob,fourth boolean)"); + assertThat( + createType("type") + .withField("first", DataTypes.TEXT) + .withField("second", DataTypes.INT) + .withField("third", DataTypes.BLOB) + .withField("fourth", DataTypes.BOOLEAN)) + .hasCql("CREATE TYPE type (first text,second int,third blob,fourth boolean)"); + } + + @Test + public void should_create_type_with_nested_UDT() { + assertThat(createType("keyspace1", "type").withField("nested", udt("val", true))) + .hasCql("CREATE TYPE keyspace1.type (nested frozen)"); + assertThat(createType("keyspace1", "type").withField("nested", udt("val", false))) + .hasCql("CREATE TYPE keyspace1.type (nested val)"); + } + + @Test + public void should_create_type_with_collections() { + assertThat(createType("ks1", "type").withField("names", DataTypes.listOf(DataTypes.TEXT))) + .hasCql("CREATE TYPE ks1.type (names list)"); + + assertThat(createType("ks1", "type").withField("names", DataTypes.tupleOf(DataTypes.TEXT))) + .hasCql("CREATE TYPE ks1.type (names frozen>)"); + + assertThat( + createType("ks1", "type") + .withField("map", DataTypes.mapOf(DataTypes.INT, DataTypes.TEXT))) + .hasCql("CREATE TYPE ks1.type (map map)"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropAggregateTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropAggregateTest.java new file mode 100644 index 00000000000..5af944238ff --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropAggregateTest.java @@ -0,0 +1,38 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.dropAggregate; + +import org.junit.Test; + +public class DropAggregateTest { + @Test + public void should_generate_drop_aggregate() { + assertThat(dropAggregate("bar")).hasCql("DROP AGGREGATE bar"); + } + + @Test + public void should_generate_drop_aggregate_with_keyspace() { + assertThat(dropAggregate("foo", "bar")).hasCql("DROP AGGREGATE foo.bar"); + } + + @Test + public void should_generate_drop_aggregate_if_exists() { + assertThat(dropAggregate("bar").ifExists()).hasCql("DROP AGGREGATE IF EXISTS bar"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropFunctionTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropFunctionTest.java new file mode 100644 index 00000000000..6a50f126805 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropFunctionTest.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.dropFunction; + +import org.junit.Test; + +public class DropFunctionTest { + + @Test + public void should_generate_drop_function() { + assertThat(dropFunction("bar")).hasCql("DROP FUNCTION bar"); + } + + @Test + public void should_generate_drop_function_with_keyspace() { + assertThat(dropFunction("foo", "bar")).hasCql("DROP FUNCTION foo.bar"); + } + + @Test + public void should_generate_drop_function_if_exists() { + assertThat(dropFunction("bar").ifExists()).hasCql("DROP FUNCTION IF EXISTS bar"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropIndexTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropIndexTest.java new file mode 100644 index 00000000000..46efe7c5f37 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropIndexTest.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.dropIndex; + +import org.junit.Test; + +public class DropIndexTest { + + @Test + public void should_generate_drop_index() { + assertThat(dropIndex("bar")).hasCql("DROP INDEX bar"); + } + + @Test + public void should_generate_drop_index_with_keyspace() { + assertThat(dropIndex("foo", "bar")).hasCql("DROP INDEX foo.bar"); + } + + @Test + public void should_generate_drop_index_if_exists() { + assertThat(dropIndex("bar").ifExists()).hasCql("DROP INDEX IF EXISTS bar"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropKeyspaceTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropKeyspaceTest.java new file mode 100644 index 00000000000..fa84c4e6783 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropKeyspaceTest.java @@ -0,0 +1,33 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.dropKeyspace; + +import org.junit.Test; + +public class DropKeyspaceTest { + @Test + public void should_generate_drop_keyspace() { + assertThat(dropKeyspace("foo")).hasCql("DROP KEYSPACE foo"); + } + + @Test + public void should_generate_drop_keyspace_if_exists() { + assertThat(dropKeyspace("foo").ifExists()).hasCql("DROP KEYSPACE IF EXISTS foo"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropMaterializedViewTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropMaterializedViewTest.java new file mode 100644 index 00000000000..a323de03c4c --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropMaterializedViewTest.java @@ -0,0 +1,40 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.dropMaterializedView; + +import org.junit.Test; + +public class DropMaterializedViewTest { + + @Test + public void should_generate_drop_view() { + assertThat(dropMaterializedView("bar")).hasCql("DROP MATERIALIZED VIEW bar"); + } + + @Test + public void should_generate_drop_view_with_keyspace() { + assertThat(dropMaterializedView("foo", "bar")).hasCql("DROP MATERIALIZED VIEW foo.bar"); + } + + @Test + public void should_generate_drop_view_if_exists() { + assertThat(dropMaterializedView("bar").ifExists()) + .hasCql("DROP MATERIALIZED VIEW IF EXISTS bar"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropTableTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropTableTest.java new file mode 100644 index 00000000000..0a986077f1c --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropTableTest.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.dropTable; + +import org.junit.Test; + +public class DropTableTest { + + @Test + public void should_generate_drop_table() { + assertThat(dropTable("bar")).hasCql("DROP TABLE bar"); + } + + @Test + public void should_generate_drop_table_with_keyspace() { + assertThat(dropTable("foo", "bar")).hasCql("DROP TABLE foo.bar"); + } + + @Test + public void should_generate_drop_table_if_exists() { + assertThat(dropTable("bar").ifExists()).hasCql("DROP TABLE IF EXISTS bar"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropTypeTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropTypeTest.java new file mode 100644 index 00000000000..cb9613f94b4 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/schema/DropTypeTest.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.schema; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.SchemaBuilder.dropType; + +import org.junit.Test; + +public class DropTypeTest { + + @Test + public void should_generate_drop_type() { + assertThat(dropType("bar")).hasCql("DROP TYPE bar"); + } + + @Test + public void should_generate_drop_type_with_keyspace() { + assertThat(dropType("foo", "bar")).hasCql("DROP TYPE foo.bar"); + } + + @Test + public void should_generate_drop_type_if_exists() { + assertThat(dropType("bar").ifExists()).hasCql("DROP TYPE IF EXISTS bar"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectAllowFilteringTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectAllowFilteringTest.java new file mode 100644 index 00000000000..79c89feeb30 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectAllowFilteringTest.java @@ -0,0 +1,35 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.select; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; + +import org.junit.Test; + +public class SelectAllowFilteringTest { + @Test + public void should_generate_allow_filtering() { + assertThat(selectFrom("foo").all().allowFiltering()) + .hasCql("SELECT * FROM foo ALLOW FILTERING"); + } + + @Test + public void should_use_single_allow_filtering_if_called_multiple_times() { + assertThat(selectFrom("foo").all().allowFiltering().allowFiltering()) + .hasCql("SELECT * FROM foo ALLOW FILTERING"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectFluentRelationTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectFluentRelationTest.java new file mode 100644 index 00000000000..884454d91f9 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectFluentRelationTest.java @@ -0,0 +1,150 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.select; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.raw; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.tuple; + +import com.datastax.oss.driver.api.querybuilder.relation.RelationTest; +import org.junit.Test; + +/** Same as {@link RelationTest}, but using {@code whereXxx()} instead of {@code where(isXxx())}. */ +public class SelectFluentRelationTest { + + @Test + public void should_generate_comparison_relation() { + assertThat(selectFrom("foo").all().whereColumn("k").isEqualTo(bindMarker())) + .hasCql("SELECT * FROM foo WHERE k=?"); + assertThat(selectFrom("foo").all().whereColumn("k").isEqualTo(bindMarker("value"))) + .hasCql("SELECT * FROM foo WHERE k=:value"); + } + + @Test + public void should_generate_is_not_null_relation() { + assertThat(selectFrom("foo").all().whereColumn("k").isNotNull()) + .hasCql("SELECT * FROM foo WHERE k IS NOT NULL"); + } + + @Test + public void should_generate_in_relation() { + assertThat(selectFrom("foo").all().whereColumn("k").in(bindMarker())) + .hasCql("SELECT * FROM foo WHERE k IN ?"); + assertThat(selectFrom("foo").all().whereColumn("k").in(bindMarker(), bindMarker())) + .hasCql("SELECT * FROM foo WHERE k IN (?,?)"); + } + + @Test + public void should_generate_token_relation() { + assertThat(selectFrom("foo").all().whereToken("k1", "k2").isEqualTo(bindMarker("t"))) + .hasCql("SELECT * FROM foo WHERE token(k1,k2)=:t"); + } + + @Test + public void should_generate_column_component_relation() { + assertThat( + selectFrom("foo") + .all() + .whereColumn("id") + .isEqualTo(bindMarker()) + .whereMapValue("user", raw("'name'")) + .isEqualTo(bindMarker())) + .hasCql("SELECT * FROM foo WHERE id=? AND user['name']=?"); + } + + @Test + public void should_generate_tuple_relation() { + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(bindMarker()) + .whereColumns("c1", "c2", "c3") + .in(bindMarker())) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3) IN ?"); + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(bindMarker()) + .whereColumns("c1", "c2", "c3") + .in(bindMarker(), bindMarker())) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3) IN (?,?)"); + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(bindMarker()) + .whereColumns("c1", "c2", "c3") + .in(bindMarker(), raw("(4,5,6)"))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3) IN (?,(4,5,6))"); + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(bindMarker()) + .whereColumns("c1", "c2", "c3") + .in( + tuple(bindMarker(), bindMarker(), bindMarker()), + tuple(bindMarker(), bindMarker(), bindMarker()))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3) IN ((?,?,?),(?,?,?))"); + + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(bindMarker()) + .whereColumns("c1", "c2", "c3") + .isEqualTo(bindMarker())) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3)=?"); + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(bindMarker()) + .whereColumns("c1", "c2", "c3") + .isLessThan(tuple(bindMarker(), bindMarker(), bindMarker()))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3)<(?,?,?)"); + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(bindMarker()) + .whereColumns("c1", "c2", "c3") + .isGreaterThanOrEqualTo(raw("(1,2,3)"))) + .hasCql("SELECT * FROM foo WHERE k=? AND (c1,c2,c3)>=(1,2,3)"); + } + + @Test + public void should_generate_custom_index_relation() { + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(bindMarker()) + .whereCustomIndex("my_index", raw("'custom expression'"))) + .hasCql("SELECT * FROM foo WHERE k=? AND expr(my_index,'custom expression')"); + } + + @Test + public void should_generate_raw_relation() { + assertThat( + selectFrom("foo").all().whereColumn("k").isEqualTo(bindMarker()).whereRaw("c = 'test'")) + .hasCql("SELECT * FROM foo WHERE k=? AND c = 'test'"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectGroupByTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectGroupByTest.java new file mode 100644 index 00000000000..12d6d2e3223 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectGroupByTest.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.select; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; + +import org.junit.Test; + +public class SelectGroupByTest { + + @Test + public void should_generate_group_by_clauses() { + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(literal(1)) + .groupBy("foo") + .groupBy("bar")) + .hasCql("SELECT * FROM foo WHERE k=1 GROUP BY foo,bar"); + + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(literal(1)) + .groupByColumns("foo", "bar")) + .hasCql("SELECT * FROM foo WHERE k=1 GROUP BY foo,bar"); + + assertThat( + selectFrom("foo") + .all() + .whereColumn("k") + .isEqualTo(literal(1)) + .groupBy( + Selector.function("ks", "f", Selector.column("foo")), Selector.column("bar"))) + .hasCql("SELECT * FROM foo WHERE k=1 GROUP BY ks.f(foo),bar"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectLimitTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectLimitTest.java new file mode 100644 index 00000000000..cba2e83d04d --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectLimitTest.java @@ -0,0 +1,50 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.select; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; + +import org.junit.Test; + +public class SelectLimitTest { + + @Test + public void should_generate_limit() { + assertThat(selectFrom("foo").all().limit(1)).hasCql("SELECT * FROM foo LIMIT 1"); + assertThat(selectFrom("foo").all().limit(bindMarker("l"))).hasCql("SELECT * FROM foo LIMIT :l"); + } + + @Test + public void should_use_last_limit_if_called_multiple_times() { + assertThat(selectFrom("foo").all().limit(1).limit(2)).hasCql("SELECT * FROM foo LIMIT 2"); + } + + @Test + public void should_generate_per_partition_limit() { + assertThat(selectFrom("foo").all().perPartitionLimit(1)) + .hasCql("SELECT * FROM foo PER PARTITION LIMIT 1"); + assertThat(selectFrom("foo").all().perPartitionLimit(bindMarker("l"))) + .hasCql("SELECT * FROM foo PER PARTITION LIMIT :l"); + } + + @Test + public void should_use_last_per_partition_limit_if_called_multiple_times() { + assertThat(selectFrom("foo").all().perPartitionLimit(1).perPartitionLimit(2)) + .hasCql("SELECT * FROM foo PER PARTITION LIMIT 2"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java new file mode 100644 index 00000000000..348c735764c --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectOrderingTest.java @@ -0,0 +1,75 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.select; + +import static com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder.ASC; +import static com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder.DESC; +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; + +import com.datastax.oss.driver.api.querybuilder.relation.Relation; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import org.junit.Test; + +public class SelectOrderingTest { + + @Test + public void should_generate_ordering_clauses() { + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(literal(1))) + .orderBy("c1", ASC) + .orderBy("c2", DESC)) + .hasCql("SELECT * FROM foo WHERE k=1 ORDER BY c1 ASC,c2 DESC"); + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(literal(1))) + .orderBy(ImmutableMap.of("c1", ASC, "c2", DESC))) + .hasCql("SELECT * FROM foo WHERE k=1 ORDER BY c1 ASC,c2 DESC"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_when_provided_names_resolve_to_the_same_id() { + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(literal(1))) + .orderBy(ImmutableMap.of("c1", ASC, "C1", DESC)); + } + + @Test + public void should_replace_previous_ordering() { + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(literal(1))) + .orderBy("c1", ASC) + .orderBy("c2", DESC) + .orderBy("c1", DESC)) + .hasCql("SELECT * FROM foo WHERE k=1 ORDER BY c2 DESC,c1 DESC"); + assertThat( + selectFrom("foo") + .all() + .where(Relation.column("k").isEqualTo(literal(1))) + .orderBy("c1", ASC) + .orderBy("c2", DESC) + .orderBy("c3", ASC) + .orderBy(ImmutableMap.of("c1", DESC, "c2", ASC))) + .hasCql("SELECT * FROM foo WHERE k=1 ORDER BY c3 ASC,c1 DESC,c2 ASC"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectSelectorTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectSelectorTest.java new file mode 100644 index 00000000000..42fa35bcd86 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/select/SelectSelectorTest.java @@ -0,0 +1,328 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.select; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.raw; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom; + +import com.datastax.oss.driver.api.core.type.DataTypes; +import com.datastax.oss.driver.api.core.type.codec.CodecNotFoundException; +import com.datastax.oss.driver.api.querybuilder.CharsetCodec; +import com.datastax.oss.driver.shaded.guava.common.base.Charsets; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import org.junit.Test; + +public class SelectSelectorTest { + + @Test + public void should_generate_star_selector() { + assertThat(selectFrom("foo").all()).hasCql("SELECT * FROM foo"); + assertThat(selectFrom("ks", "foo").all()).hasCql("SELECT * FROM ks.foo"); + } + + @Test + public void should_remove_star_selector_if_other_selector_added() { + assertThat(selectFrom("foo").all().column("bar")).hasCql("SELECT bar FROM foo"); + } + + @Test + public void should_remove_other_selectors_if_star_selector_added() { + assertThat(selectFrom("foo").column("bar").column("baz").all()).hasCql("SELECT * FROM foo"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_if_selector_list_contains_star_selector() { + selectFrom("foo").selectors(Selector.column("bar"), Selector.all(), raw("baz")); + } + + @Test + public void should_generate_count_all_selector() { + assertThat(selectFrom("foo").countAll()).hasCql("SELECT count(*) FROM foo"); + } + + @Test + public void should_generate_column_selectors() { + assertThat(selectFrom("foo").column("bar")).hasCql("SELECT bar FROM foo"); + assertThat(selectFrom("foo").column("bar").column("baz")).hasCql("SELECT bar,baz FROM foo"); + assertThat(selectFrom("foo").selectors(Selector.column("bar"), Selector.column("baz"))) + .hasCql("SELECT bar,baz FROM foo"); + assertThat(selectFrom("foo").columns("a", "b", "c")).hasCql("SELECT a,b,c FROM foo"); + } + + @Test + public void should_generate_arithmetic_selectors() { + assertThat(selectFrom("foo").add(Selector.column("bar"), Selector.column("baz"))) + .hasCql("SELECT bar+baz FROM foo"); + assertThat( + selectFrom("foo") + .subtract(raw("1"), Selector.add(Selector.column("bar"), Selector.column("baz")))) + .hasCql("SELECT 1-(bar+baz) FROM foo"); + assertThat( + selectFrom("foo").negate(Selector.add(Selector.column("bar"), Selector.column("baz")))) + .hasCql("SELECT -(bar+baz) FROM foo"); + assertThat( + selectFrom("foo") + .multiply( + Selector.negate(Selector.column("bar")), + Selector.add(Selector.column("baz"), literal(1)))) + .hasCql("SELECT -bar*(baz+1) FROM foo"); + assertThat( + selectFrom("foo") + .divide(literal(1), Selector.add(Selector.column("bar"), Selector.column("baz")))) + .hasCql("SELECT 1/(bar+baz) FROM foo"); + assertThat( + selectFrom("foo") + .divide( + literal(1), Selector.multiply(Selector.column("bar"), Selector.column("baz")))) + .hasCql("SELECT 1/(bar*baz) FROM foo"); + } + + @Test + public void should_generate_field_selectors() { + assertThat(selectFrom("foo").field("user", "name")).hasCql("SELECT user.name FROM foo"); + assertThat(selectFrom("foo").field(Selector.field("user", "address"), "city")) + .hasCql("SELECT user.address.city FROM foo"); + } + + @Test + public void should_generate_element_selectors() { + assertThat(selectFrom("foo").element("m", literal(1))).hasCql("SELECT m[1] FROM foo"); + assertThat(selectFrom("foo").element(Selector.element("m", literal("bar")), literal(1))) + .hasCql("SELECT m['bar'][1] FROM foo"); + } + + @Test + public void should_generate_range_selectors() { + assertThat(selectFrom("foo").range("s", literal(1), literal(5))) + .hasCql("SELECT s[1..5] FROM foo"); + assertThat(selectFrom("foo").range("s", literal(1), null)).hasCql("SELECT s[1..] FROM foo"); + assertThat(selectFrom("foo").range("s", null, literal(5))).hasCql("SELECT s[..5] FROM foo"); + } + + @Test + public void should_generate_collection_and_tuple_selectors() { + assertThat( + selectFrom("foo") + .listOf(Selector.column("a"), Selector.column("b"), Selector.column("c"))) + .hasCql("SELECT [a,b,c] FROM foo"); + assertThat( + selectFrom("foo") + .setOf(Selector.column("a"), Selector.column("b"), Selector.column("c"))) + .hasCql("SELECT {a,b,c} FROM foo"); + assertThat( + selectFrom("foo") + .tupleOf(Selector.column("a"), Selector.column("b"), Selector.column("c"))) + .hasCql("SELECT (a,b,c) FROM foo"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_if_collection_selector_contains_aliases() { + selectFrom("foo") + .listOf( + Selector.column("a"), Selector.column("b").as("FORBIDDEN_HERE"), Selector.column("c")); + } + + @Test + public void should_generate_map_selectors() { + assertThat( + selectFrom("foo") + .mapOf( + ImmutableMap.of( + Selector.column("k1"), + Selector.column("v1"), + Selector.column("k2"), + Selector.column("v2")))) + .hasCql("SELECT {k1:v1,k2:v2} FROM foo"); + assertThat( + selectFrom("foo") + .mapOf( + ImmutableMap.of( + Selector.column("k1"), + Selector.column("v1"), + Selector.column("k2"), + Selector.column("v2")), + DataTypes.TEXT, + DataTypes.INT)) + .hasCql("SELECT (map){k1:v1,k2:v2} FROM foo"); + } + + @Test(expected = IllegalArgumentException.class) + public void should_fail_if_map_selector_contains_aliases() { + selectFrom("foo") + .mapOf( + ImmutableMap.of( + Selector.column("k1"), + Selector.column("v1").as("FORBIDDEN_HERE"), + Selector.column("k2"), + Selector.column("v2"))); + } + + @Test + public void should_generate_type_hint_selector() { + assertThat(selectFrom("foo").typeHint(Selector.column("k"), DataTypes.INT)) + .hasCql("SELECT (int)k FROM foo"); + } + + @Test + public void should_generate_function_selectors() { + assertThat( + selectFrom("foo") + .function( + "f", Selector.column("c1"), Selector.add(Selector.column("c2"), raw("1")))) + .hasCql("SELECT f(c1,c2+1) FROM foo"); + assertThat( + selectFrom("foo") + .function( + "ks", + "f", + Selector.column("c1"), + Selector.add(Selector.column("c2"), raw("1")))) + .hasCql("SELECT ks.f(c1,c2+1) FROM foo"); + assertThat(selectFrom("foo").writeTime("c1").ttl("c2")) + .hasCql("SELECT writetime(c1),ttl(c2) FROM foo"); + assertThat(selectFrom("foo").toDate("a").toTimestamp("b").toUnixTimestamp("c")) + .hasCql("SELECT todate(a),totimestamp(b),tounixtimestamp(c) FROM foo"); + } + + @Test + public void should_generate_cast_selector() { + assertThat(selectFrom("foo").cast(Selector.column("k"), DataTypes.DOUBLE)) + .hasCql("SELECT CAST(k AS double) FROM foo"); + } + + @Test + public void should_generate_literal_selectors() { + assertThat(selectFrom("foo").literal(1)).hasCql("SELECT 1 FROM foo"); + assertThat(selectFrom("foo").literal(Charsets.UTF_8, new CharsetCodec())) + .hasCql("SELECT 'UTF-8' FROM foo"); + assertThat(selectFrom("foo").literal(Charsets.UTF_8, CharsetCodec.TEST_REGISTRY)) + .hasCql("SELECT 'UTF-8' FROM foo"); + assertThat(selectFrom("foo").literal(null)).hasCql("SELECT NULL FROM foo"); + } + + @Test(expected = CodecNotFoundException.class) + public void should_fail_when_no_codec_for_literal() { + selectFrom("foo").literal(Charsets.UTF_8); + } + + @Test + public void should_generate_raw_selector() { + assertThat(selectFrom("foo").raw("a,b,c")).hasCql("SELECT a,b,c FROM foo"); + + assertThat(selectFrom("foo").selectors(Selector.column("bar"), raw("baz"))) + .hasCql("SELECT bar,baz FROM foo"); + } + + @Test + public void should_alias_selectors() { + assertThat(selectFrom("foo").column("bar").as("baz")).hasCql("SELECT bar AS baz FROM foo"); + assertThat( + selectFrom("foo") + .selectors(Selector.column("bar").as("c1"), Selector.column("baz").as("c2"))) + .hasCql("SELECT bar AS c1,baz AS c2 FROM foo"); + } + + @Test(expected = IllegalStateException.class) + public void should_fail_to_alias_star_selector() { + selectFrom("foo").all().as("allthethings"); + } + + @Test(expected = IllegalStateException.class) + public void should_fail_to_alias_if_no_selector_yet() { + selectFrom("foo").as("bar"); + } + + @Test + public void should_keep_last_alias_if_aliased_twice() { + assertThat(selectFrom("foo").countAll().as("allthethings").as("total")) + .hasCql("SELECT count(*) AS total FROM foo"); + } + + @Test + public void should_alias_function_selector() { + assertThat(selectFrom("foo").function("bar", Selector.column("col")).as("alias_1")) + .hasCql("SELECT bar(col) AS alias_1 FROM foo"); + + assertThat( + selectFrom("foo") + .function("bar", Selector.column("col")) + .as("alias_1") + .function("baz", Selector.column("col")) + .as("alias_2")) + .hasCql("SELECT bar(col) AS alias_1,baz(col) AS alias_2 FROM foo"); + } + + @Test + public void should_alias_list_selector() { + assertThat(selectFrom("foo").listOf(Selector.column("col")).as("alias_1")) + .hasCql("SELECT [col] AS alias_1 FROM foo"); + + assertThat( + selectFrom("foo") + .listOf(Selector.column("col")) + .as("alias_1") + .listOf(Selector.column("col2")) + .as("alias_2")) + .hasCql("SELECT [col] AS alias_1,[col2] AS alias_2 FROM foo"); + } + + @Test + public void should_alias_set_selector() { + assertThat(selectFrom("foo").setOf(Selector.column("col")).as("alias_1")) + .hasCql("SELECT {col} AS alias_1 FROM foo"); + + assertThat( + selectFrom("foo") + .setOf(Selector.column("col")) + .as("alias_1") + .setOf(Selector.column("col2")) + .as("alias_2")) + .hasCql("SELECT {col} AS alias_1,{col2} AS alias_2 FROM foo"); + } + + @Test + public void should_alias_tuple_selector() { + assertThat(selectFrom("foo").tupleOf(Selector.column("col")).as("alias_1")) + .hasCql("SELECT (col) AS alias_1 FROM foo"); + + assertThat( + selectFrom("foo") + .tupleOf(Selector.column("col")) + .as("alias_1") + .tupleOf(Selector.column("col2")) + .as("alias_2")) + .hasCql("SELECT (col) AS alias_1,(col2) AS alias_2 FROM foo"); + } + + @Test + public void should_alias_map_selector() { + assertThat( + selectFrom("foo") + .mapOf(ImmutableMap.of(Selector.column("a"), Selector.column("b"))) + .as("alias_1")) + .hasCql("SELECT {a:b} AS alias_1 FROM foo"); + + assertThat( + selectFrom("foo") + .mapOf(ImmutableMap.of(Selector.column("a"), Selector.column("b"))) + .as("alias_1") + .mapOf(ImmutableMap.of(Selector.column("c"), Selector.column("d"))) + .as("alias_2")) + .hasCql("SELECT {a:b} AS alias_1,{c:d} AS alias_2 FROM foo"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateFluentAssignmentTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateFluentAssignmentTest.java new file mode 100644 index 00000000000..184ad2e2dbf --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateFluentAssignmentTest.java @@ -0,0 +1,193 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.update; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.update; + +import com.datastax.oss.driver.api.querybuilder.Literal; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap; +import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet; +import org.junit.Test; + +public class UpdateFluentAssignmentTest { + + @Test + public void should_generate_simple_column_assignment() { + assertThat(update("foo").setColumn("v", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET v=? WHERE k=?"); + assertThat( + update("ks", "foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE ks.foo SET v=? WHERE k=?"); + } + + @Test + public void should_generate_field_assignment() { + assertThat( + update("foo") + .setField("address", "street", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET address.street=? WHERE k=?"); + } + + @Test + public void should_generate_map_value_assignment() { + assertThat( + update("foo") + .setMapValue("features", literal("color"), bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET features['color']=? WHERE k=?"); + } + + @Test + public void should_generate_counter_operations() { + assertThat(update("foo").increment("c").whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET c+=1 WHERE k=?"); + assertThat(update("foo").increment("c", literal(2)).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET c+=2 WHERE k=?"); + assertThat(update("foo").increment("c", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET c+=? WHERE k=?"); + + assertThat(update("foo").decrement("c").whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET c-=1 WHERE k=?"); + assertThat(update("foo").decrement("c", literal(2)).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET c-=2 WHERE k=?"); + assertThat(update("foo").decrement("c", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET c-=? WHERE k=?"); + } + + @Test + public void should_generate_list_operations() { + Literal listLiteral = literal(ImmutableList.of(1, 2, 3)); + + assertThat(update("foo").append("l", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l+=? WHERE k=?"); + assertThat(update("foo").append("l", listLiteral).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l+=[1,2,3] WHERE k=?"); + assertThat( + update("foo") + .appendListElement("l", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l+=[?] WHERE k=?"); + + assertThat(update("foo").prepend("l", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l=?+l WHERE k=?"); + assertThat(update("foo").prepend("l", listLiteral).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l=[1,2,3]+l WHERE k=?"); + assertThat( + update("foo") + .prependListElement("l", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l=[?]+l WHERE k=?"); + + assertThat(update("foo").remove("l", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l-=? WHERE k=?"); + assertThat(update("foo").remove("l", listLiteral).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l-=[1,2,3] WHERE k=?"); + assertThat( + update("foo") + .removeListElement("l", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l-=[?] WHERE k=?"); + } + + @Test + public void should_generate_set_operations() { + Literal setLiteral = literal(ImmutableSet.of(1, 2, 3)); + + assertThat(update("foo").append("s", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET s+=? WHERE k=?"); + assertThat(update("foo").append("s", setLiteral).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET s+={1,2,3} WHERE k=?"); + assertThat( + update("foo") + .appendSetElement("s", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET s+={?} WHERE k=?"); + + assertThat(update("foo").prepend("s", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET s=?+s WHERE k=?"); + assertThat(update("foo").prepend("s", setLiteral).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET s={1,2,3}+s WHERE k=?"); + assertThat( + update("foo") + .prependSetElement("s", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET s={?}+s WHERE k=?"); + + assertThat(update("foo").remove("s", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET s-=? WHERE k=?"); + assertThat(update("foo").remove("s", setLiteral).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET s-={1,2,3} WHERE k=?"); + assertThat( + update("foo") + .removeSetElement("s", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET s-={?} WHERE k=?"); + } + + @Test + public void should_generate_map_operations() { + Literal mapLiteral = literal(ImmutableMap.of(1, "foo", 2, "bar")); + + assertThat(update("foo").append("m", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET m+=? WHERE k=?"); + assertThat(update("foo").append("m", mapLiteral).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET m+={1:'foo',2:'bar'} WHERE k=?"); + assertThat( + update("foo") + .appendMapEntry("m", literal(1), literal("foo")) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET m+={1:'foo'} WHERE k=?"); + + assertThat(update("foo").prepend("m", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET m=?+m WHERE k=?"); + assertThat(update("foo").prepend("m", mapLiteral).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET m={1:'foo',2:'bar'}+m WHERE k=?"); + assertThat( + update("foo") + .prependMapEntry("m", literal(1), literal("foo")) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET m={1:'foo'}+m WHERE k=?"); + + assertThat(update("foo").remove("m", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET m-=? WHERE k=?"); + assertThat(update("foo").remove("m", mapLiteral).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET m-={1:'foo',2:'bar'} WHERE k=?"); + assertThat( + update("foo") + .removeMapEntry("m", literal(1), literal("foo")) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET m-={1:'foo'} WHERE k=?"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateFluentConditionTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateFluentConditionTest.java new file mode 100644 index 00000000000..8a562431510 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateFluentConditionTest.java @@ -0,0 +1,111 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.update; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.update; + +import org.junit.Test; + +public class UpdateFluentConditionTest { + + @Test + public void should_generate_simple_column_condition() { + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifColumn("v") + .isEqualTo(literal(1))) + .hasCql("UPDATE foo SET v=? WHERE k=? IF v=1"); + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifColumn("v1") + .isEqualTo(literal(1)) + .ifColumn("v2") + .isEqualTo(literal(2))) + .hasCql("UPDATE foo SET v=? WHERE k=? IF v1=1 AND v2=2"); + } + + @Test + public void should_generate_field_condition() { + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifField("v", "f") + .isEqualTo(literal(1))) + .hasCql("UPDATE foo SET v=? WHERE k=? IF v.f=1"); + } + + @Test + public void should_generate_element_condition() { + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifElement("v", literal(1)) + .isEqualTo(literal(1))) + .hasCql("UPDATE foo SET v=? WHERE k=? IF v[1]=1"); + } + + @Test + public void should_generate_if_exists_condition() { + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifExists()) + .hasCql("UPDATE foo SET v=? WHERE k=? IF EXISTS"); + } + + @Test + public void should_cancel_if_exists_if_other_condition_added() { + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifExists() + .ifColumn("v") + .isEqualTo(literal(1))) + .hasCql("UPDATE foo SET v=? WHERE k=? IF v=1"); + } + + @Test + public void should_cancel_other_conditions_if_if_exists_added() { + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifColumn("v1") + .isEqualTo(literal(1)) + .ifColumn("v2") + .isEqualTo(literal(2)) + .ifExists()) + .hasCql("UPDATE foo SET v=? WHERE k=? IF EXISTS"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateFluentRelationTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateFluentRelationTest.java new file mode 100644 index 00000000000..86559ea65f1 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateFluentRelationTest.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.update; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.update; + +import com.datastax.oss.driver.api.querybuilder.relation.RelationTest; +import com.datastax.oss.driver.api.querybuilder.select.SelectFluentRelationTest; +import org.junit.Test; + +/** + * Mostly covered by other tests already. + * + * @see SelectFluentRelationTest + * @see RelationTest + */ +public class UpdateFluentRelationTest { + + @Test + public void should_generate_update_with_column_relation() { + assertThat(update("foo").setColumn("v", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET v=? WHERE k=?"); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateIdempotenceTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateIdempotenceTest.java new file mode 100644 index 00000000000..1f424484ee6 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateIdempotenceTest.java @@ -0,0 +1,151 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.update; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.function; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.raw; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.update; + +import java.util.Arrays; +import org.junit.Test; + +public class UpdateIdempotenceTest { + + @Test + public void should_not_be_idempotent_if_conditional() { + assertThat(update("foo").setColumn("v", bindMarker()).whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET v=? WHERE k=?") + .isIdempotent(); + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifExists()) + .hasCql("UPDATE foo SET v=? WHERE k=? IF EXISTS") + .isNotIdempotent(); + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker()) + .ifColumn("v") + .isEqualTo(literal(1))) + .hasCql("UPDATE foo SET v=? WHERE k=? IF v=1") + .isNotIdempotent(); + } + + @Test + public void should_not_be_idempotent_if_assigning_non_idempotent_term() { + assertThat( + update("foo") + .setColumn("v", function("non_idempotent_func")) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET v=non_idempotent_func() WHERE k=?") + .isNotIdempotent(); + assertThat( + update("foo") + .setColumn("v", raw("non_idempotent_func()")) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET v=non_idempotent_func() WHERE k=?") + .isNotIdempotent(); + } + + @Test + public void should_not_be_idempotent_if_using_non_idempotent_term_in_relation() { + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(function("non_idempotent_func"))) + .hasCql("UPDATE foo SET v=? WHERE k=non_idempotent_func()") + .isNotIdempotent(); + assertThat( + update("foo") + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(raw("non_idempotent_func()"))) + .hasCql("UPDATE foo SET v=? WHERE k=non_idempotent_func()") + .isNotIdempotent(); + } + + @Test + public void should_not_be_idempotent_if_updating_counter() { + assertThat(update("foo").increment("c").whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET c+=1 WHERE k=?") + .isNotIdempotent(); + assertThat(update("foo").decrement("c").whereColumn("k").isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET c-=1 WHERE k=?") + .isNotIdempotent(); + } + + @Test + public void should_not_be_idempotent_if_adding_element_to_list() { + assertThat( + update("foo") + .appendListElement("l", literal(1)) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l+=[1] WHERE k=?") + .isNotIdempotent(); + assertThat( + update("foo") + .prependListElement("l", literal(1)) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l=[1]+l WHERE k=?") + .isNotIdempotent(); + + // On the other hand, other collections are safe: + assertThat( + update("foo") + .appendSetElement("s", literal(1)) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET s+={1} WHERE k=?") + .isIdempotent(); + assertThat( + update("foo") + .appendMapEntry("m", literal(1), literal("bar")) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET m+={1:'bar'} WHERE k=?") + .isIdempotent(); + } + + @Test + public void should_not_be_idempotent_if_concatenating_to_collection() { + assertThat( + update("foo") + .append("l", literal(Arrays.asList(1, 2, 3))) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l+=[1,2,3] WHERE k=?") + .isNotIdempotent(); + assertThat( + update("foo") + .prepend("l", literal(Arrays.asList(1, 2, 3))) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo SET l=[1,2,3]+l WHERE k=?") + .isNotIdempotent(); + } +} diff --git a/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateTimestampTest.java b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateTimestampTest.java new file mode 100644 index 00000000000..6ae165bfa19 --- /dev/null +++ b/query-builder/src/test/java/com/datastax/oss/driver/api/querybuilder/update/UpdateTimestampTest.java @@ -0,0 +1,56 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.querybuilder.update; + +import static com.datastax.oss.driver.api.querybuilder.Assertions.assertThat; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.update; + +import org.junit.Test; + +public class UpdateTimestampTest { + + @Test + public void should_generate_using_timestamp_clause() { + assertThat( + update("foo") + .usingTimestamp(1) + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo USING TIMESTAMP 1 SET v=? WHERE k=?"); + assertThat( + update("foo") + .usingTimestamp(bindMarker()) + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo USING TIMESTAMP ? SET v=? WHERE k=?"); + } + + @Test + public void should_use_last_timestamp_if_called_multiple_times() { + assertThat( + update("foo") + .usingTimestamp(1) + .usingTimestamp(2) + .usingTimestamp(3) + .setColumn("v", bindMarker()) + .whereColumn("k") + .isEqualTo(bindMarker())) + .hasCql("UPDATE foo USING TIMESTAMP 3 SET v=? WHERE k=?"); + } +} diff --git a/test-infra/pom.xml b/test-infra/pom.xml new file mode 100644 index 00000000000..9d3998c5294 --- /dev/null +++ b/test-infra/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + + com.datastax.oss + java-driver-parent + 4.0.0 + + + java-driver-test-infra + bundle + + DataStax Java driver for Apache Cassandra(R) - test infrastructure tools + + + + com.datastax.oss + java-driver-core + ${project.parent.version} + + + com.github.spotbugs + spotbugs-annotations + true + + + junit + junit + + + org.assertj + assertj-core + + + com.datastax.oss.simulacron + simulacron-native-server + + + org.apache.commons + commons-exec + + + + + + + org.apache.felix + maven-bundle-plugin + + + com.datastax.oss.driver.testinfra + + * + + com.datastax.oss.driver.*.testinfra.*, + com.datastax.oss.driver.assertions, + com.datastax.oss.driver.categories + + + + + + + diff --git a/test-infra/revapi.json b/test-infra/revapi.json new file mode 100644 index 00000000000..179c6f1df52 --- /dev/null +++ b/test-infra/revapi.json @@ -0,0 +1,42 @@ +// Configures Revapi (https://revapi.org/getting-started.html) to check API compatibility between +// successive driver versions. +{ + "revapi": { + "java": { + "filter": { + "packages": { + "regex": true, + "exclude": [ + "com\\.datastax\\.oss\\.protocol\\.internal(\\..+)?", + "com\\.datastax\\.oss\\.driver\\.internal(\\..+)?", + "com\\.datastax\\.oss\\.driver\\.shaded(\\..+)?", + "com\\.datastax\\.oss\\.simulacron(\\..+)?", + "org\\.assertj(\\..+)?", + // Don't re-check sibling modules that this module depends on + "com\\.datastax\\.oss\\.driver\\.api\\.core(\\..+)?" + ] + } + } + }, + "ignore": [ + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Set com.datastax.oss.driver.api.testinfra.CassandraResourceRule::getContactPoints()", + "new": "method java.util.Set com.datastax.oss.driver.api.testinfra.CassandraResourceRule::getContactPoints()", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.numberOfParametersChanged", + "old": "method void com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::init(java.util.Map, com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy.DistanceReporter, java.util.Set)", + "new": "method void com.datastax.oss.driver.api.testinfra.loadbalancing.SortingLoadBalancingPolicy::init(java.util.Map, com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy.DistanceReporter)", + "justification": "JAVA-2165: Abstract node connection information" + }, + { + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.util.Set com.datastax.oss.driver.api.testinfra.simulacron.SimulacronRule::getContactPoints()", + "new": "method java.util.Set com.datastax.oss.driver.api.testinfra.simulacron.SimulacronRule::getContactPoints()", + "justification": "JAVA-2165: Abstract node connection information" + } + ] + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/CassandraRequirement.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/CassandraRequirement.java new file mode 100644 index 00000000000..e28757e420f --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/CassandraRequirement.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotation for a Class or Method that defines a Cassandra Version requirement. If the cassandra + * version in use does not meet the version requirement, the test is skipped. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface CassandraRequirement { + + /** @return The minimum version required to execute this test, i.e. "2.0.13" */ + String min() default ""; + + /** + * @return the maximum exclusive version allowed to execute this test, i.e. "2.2" means only tests + * < "2.2" may execute this test. + */ + String max() default ""; + + /** @return The description returned if this version requirement is not met. */ + String description() default "Does not meet version requirement."; +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/CassandraResourceRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/CassandraResourceRule.java new file mode 100644 index 00000000000..4e40f2788f7 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/CassandraResourceRule.java @@ -0,0 +1,59 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra; + +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.testinfra.session.SessionRule; +import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.Set; +import org.junit.rules.ExternalResource; +import org.junit.rules.RuleChain; + +/** + * An {@link ExternalResource} which provides a {@link #getContactPoints()} for accessing the + * contact points of the cassandra cluster. + */ +public abstract class CassandraResourceRule extends ExternalResource { + + /** + * @deprecated this method is preserved for backward compatibility only. The correct way to ensure + * that a {@code CassandraResourceRule} gets initialized before a {@link SessionRule} is to + * wrap them into a {@link RuleChain}. Therefore there is no need to force the initialization + * of a {@code CassandraResourceRule} explicitly anymore. + */ + @Deprecated + public synchronized void setUp() { + try { + this.before(); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + /** + * @return Default contact points associated with this cassandra resource. By default returns + * 127.0.0.1 + */ + public Set getContactPoints() { + return Collections.singleton(new DefaultEndPoint(new InetSocketAddress("127.0.0.1", 9042))); + } + + /** @return The highest protocol version supported by this resource. */ + public abstract ProtocolVersion getHighestProtocolVersion(); +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/DseRequirement.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/DseRequirement.java new file mode 100644 index 00000000000..c80c6914282 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/DseRequirement.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Annotation for a Class or Method that defines a DSE Version requirement. If the DSE version in + * use does not meet the version requirement or DSE isn't used at all, the test is skipped. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface DseRequirement { + + /** @return The minimum version required to execute this test, i.e. "5.0.13" */ + String min() default ""; + + /** + * @return the maximum exclusive version allowed to execute this test, i.e. "2.2" means only tests + * < "2.2" may execute this test. + */ + String max() default ""; + + /** @return The description returned if this version requirement is not met. */ + String description() default "Does not meet version requirement."; +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java new file mode 100644 index 00000000000..42c754f0dba --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/BaseCcmRule.java @@ -0,0 +1,150 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.ccm; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.api.testinfra.CassandraRequirement; +import com.datastax.oss.driver.api.testinfra.CassandraResourceRule; +import com.datastax.oss.driver.api.testinfra.DseRequirement; +import java.util.Optional; +import org.junit.AssumptionViolatedException; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public abstract class BaseCcmRule extends CassandraResourceRule { + + protected final CcmBridge ccmBridge; + + BaseCcmRule(CcmBridge ccmBridge) { + this.ccmBridge = ccmBridge; + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + try { + ccmBridge.remove(); + } catch (Exception e) { + // silently remove as may have already been removed. + } + })); + } + + @Override + protected void before() { + ccmBridge.create(); + ccmBridge.start(); + } + + @Override + protected void after() { + ccmBridge.remove(); + } + + private Statement buildErrorStatement( + Version requirement, String description, boolean lessThan, boolean dse) { + return new Statement() { + + @Override + public void evaluate() { + throw new AssumptionViolatedException( + String.format( + "Test requires %s %s %s but %s is configured. Description: %s", + lessThan ? "less than" : "at least", + dse ? "DSE" : "C*", + requirement, + dse ? ccmBridge.getDseVersion().orElse(null) : ccmBridge.getCassandraVersion(), + description)); + } + }; + } + + @Override + public Statement apply(Statement base, Description description) { + // If test is annotated with CassandraRequirement or DseRequirement, ensure configured CCM + // cluster meets those requirements. + CassandraRequirement cassandraRequirement = + description.getAnnotation(CassandraRequirement.class); + + if (cassandraRequirement != null) { + // if the configured cassandra cassandraRequirement exceeds the one being used skip this test. + if (!cassandraRequirement.min().isEmpty()) { + Version minVersion = Version.parse(cassandraRequirement.min()); + if (minVersion.compareTo(ccmBridge.getCassandraVersion()) > 0) { + return buildErrorStatement(minVersion, cassandraRequirement.description(), false, false); + } + } + + if (!cassandraRequirement.max().isEmpty()) { + // if the test version exceeds the maximum configured one, fail out. + Version maxVersion = Version.parse(cassandraRequirement.max()); + + if (maxVersion.compareTo(ccmBridge.getCassandraVersion()) <= 0) { + return buildErrorStatement(maxVersion, cassandraRequirement.description(), true, false); + } + } + } + + DseRequirement dseRequirement = description.getAnnotation(DseRequirement.class); + if (dseRequirement != null) { + Optional dseVersionOption = ccmBridge.getDseVersion(); + if (!dseVersionOption.isPresent()) { + return new Statement() { + + @Override + public void evaluate() { + throw new AssumptionViolatedException("Test Requires DSE but C* is configured."); + } + }; + } else { + Version dseVersion = dseVersionOption.get(); + if (!dseRequirement.min().isEmpty()) { + Version minVersion = Version.parse(dseRequirement.min()); + if (minVersion.compareTo(dseVersion) > 0) { + return buildErrorStatement(minVersion, dseRequirement.description(), false, true); + } + } + + if (!dseRequirement.max().isEmpty()) { + Version maxVersion = Version.parse(dseRequirement.max()); + + if (maxVersion.compareTo(ccmBridge.getCassandraVersion()) <= 0) { + return buildErrorStatement(maxVersion, dseRequirement.description(), true, true); + } + } + } + } + return super.apply(base, description); + } + + public Version getCassandraVersion() { + return ccmBridge.getCassandraVersion(); + } + + public Optional getDseVersion() { + return ccmBridge.getDseVersion(); + } + + @Override + public ProtocolVersion getHighestProtocolVersion() { + if (ccmBridge.getCassandraVersion().compareTo(Version.V2_2_0) >= 0) { + return DefaultProtocolVersion.V4; + } else { + return DefaultProtocolVersion.V3; + } + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java new file mode 100644 index 00000000000..54783a3d664 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmBridge.java @@ -0,0 +1,468 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.ccm; + +import static io.netty.util.internal.PlatformDependent.isWindows; + +import com.datastax.oss.driver.api.core.Version; +import com.datastax.oss.driver.shaded.guava.common.base.Joiner; +import com.datastax.oss.driver.shaded.guava.common.io.Resources; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import org.apache.commons.exec.CommandLine; +import org.apache.commons.exec.DefaultExecutor; +import org.apache.commons.exec.ExecuteStreamHandler; +import org.apache.commons.exec.ExecuteWatchdog; +import org.apache.commons.exec.Executor; +import org.apache.commons.exec.LogOutputStream; +import org.apache.commons.exec.PumpStreamHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CcmBridge implements AutoCloseable { + + private static final Logger logger = LoggerFactory.getLogger(CcmBridge.class); + + private final int[] nodes; + + private final Path configDirectory; + + private final AtomicBoolean started = new AtomicBoolean(); + + private final AtomicBoolean created = new AtomicBoolean(); + + private final String ipPrefix; + + private final Map cassandraConfiguration; + private final Map dseConfiguration; + private final List rawDseYaml; + private final List createOptions; + private final List dseWorkloads; + + private final String jvmArgs; + + public static final Version VERSION = Version.parse(System.getProperty("ccm.version", "3.11.0")); + + public static final String INSTALL_DIRECTORY = System.getProperty("ccm.directory"); + + public static final String BRANCH = System.getProperty("ccm.branch"); + + public static final Boolean DSE_ENABLEMENT = Boolean.getBoolean("ccm.dse"); + + public static final String DEFAULT_CLIENT_TRUSTSTORE_PASSWORD = "cassandra1sfun"; + public static final String DEFAULT_CLIENT_TRUSTSTORE_PATH = "/client.truststore"; + + public static final File DEFAULT_CLIENT_TRUSTSTORE_FILE = + createTempStore(DEFAULT_CLIENT_TRUSTSTORE_PATH); + + public static final String DEFAULT_CLIENT_KEYSTORE_PASSWORD = "cassandra1sfun"; + public static final String DEFAULT_CLIENT_KEYSTORE_PATH = "/client.keystore"; + + public static final File DEFAULT_CLIENT_KEYSTORE_FILE = + createTempStore(DEFAULT_CLIENT_KEYSTORE_PATH); + + // Contains the same keypair as the client keystore, but in format usable by OpenSSL + public static final File DEFAULT_CLIENT_PRIVATE_KEY_FILE = createTempStore("/client.key"); + public static final File DEFAULT_CLIENT_CERT_CHAIN_FILE = createTempStore("/client.crt"); + + public static final String DEFAULT_SERVER_TRUSTSTORE_PASSWORD = "cassandra1sfun"; + public static final String DEFAULT_SERVER_TRUSTSTORE_PATH = "/server.truststore"; + + private static final File DEFAULT_SERVER_TRUSTSTORE_FILE = + createTempStore(DEFAULT_SERVER_TRUSTSTORE_PATH); + + public static final String DEFAULT_SERVER_KEYSTORE_PASSWORD = "cassandra1sfun"; + public static final String DEFAULT_SERVER_KEYSTORE_PATH = "/server.keystore"; + + private static final File DEFAULT_SERVER_KEYSTORE_FILE = + createTempStore(DEFAULT_SERVER_KEYSTORE_PATH); + + // A separate keystore where the certificate has a CN of localhost, used for hostname + // validation testing. + public static final String DEFAULT_SERVER_LOCALHOST_KEYSTORE_PATH = "/server_localhost.keystore"; + + private static final File DEFAULT_SERVER_LOCALHOST_KEYSTORE_FILE = + createTempStore(DEFAULT_SERVER_LOCALHOST_KEYSTORE_PATH); + + // major DSE versions + private static final Version V6_0_0 = Version.parse("6.0.0"); + private static final Version V5_1_0 = Version.parse("5.1.0"); + private static final Version V5_0_0 = Version.parse("5.0.0"); + + // mapped C* versions from DSE versions + private static final Version V4_0_0 = Version.parse("4.0.0"); + private static final Version V3_10 = Version.parse("3.10"); + private static final Version V3_0_15 = Version.parse("3.0.15"); + private static final Version V2_1_19 = Version.parse("2.1.19"); + + private CcmBridge( + Path configDirectory, + int[] nodes, + String ipPrefix, + Map cassandraConfiguration, + Map dseConfiguration, + List dseConfigurationRawYaml, + List createOptions, + Collection jvmArgs, + List dseWorkloads) { + this.configDirectory = configDirectory; + if (nodes.length == 1) { + // Hack to ensure that the default DC is always called 'dc1': pass a list ('-nX:0') even if + // there is only one DC (with '-nX', CCM configures `SimpleSnitch`, which hard-codes the name + // to 'datacenter1') + int[] tmp = new int[2]; + tmp[0] = nodes[0]; + tmp[1] = 0; + this.nodes = tmp; + } else { + this.nodes = nodes; + } + this.ipPrefix = ipPrefix; + this.cassandraConfiguration = cassandraConfiguration; + this.dseConfiguration = dseConfiguration; + this.rawDseYaml = dseConfigurationRawYaml; + this.createOptions = createOptions; + + StringBuilder allJvmArgs = new StringBuilder(""); + String quote = isWindows() ? "\"" : ""; + for (String jvmArg : jvmArgs) { + // Windows requires jvm arguments to be quoted, while *nix requires unquoted. + allJvmArgs.append(" "); + allJvmArgs.append(quote); + allJvmArgs.append("--jvm_arg="); + allJvmArgs.append(jvmArg); + allJvmArgs.append(quote); + } + this.jvmArgs = allJvmArgs.toString(); + this.dseWorkloads = dseWorkloads; + } + + public Optional getDseVersion() { + return DSE_ENABLEMENT ? Optional.of(VERSION) : Optional.empty(); + } + + public Version getCassandraVersion() { + if (!DSE_ENABLEMENT) { + return VERSION; + } else { + Version stableVersion = VERSION.nextStable(); + if (stableVersion.compareTo(V6_0_0) >= 0) { + return V4_0_0; + } else if (stableVersion.compareTo(V5_1_0) >= 0) { + return V3_10; + } else if (stableVersion.compareTo(V5_0_0) >= 0) { + return V3_0_15; + } else { + return V2_1_19; + } + } + } + + public void create() { + if (created.compareAndSet(false, true)) { + if (INSTALL_DIRECTORY != null) { + createOptions.add("--install-dir=" + new File(INSTALL_DIRECTORY).getAbsolutePath()); + } else if (BRANCH != null) { + createOptions.add("-v git:" + BRANCH.trim().replaceAll("\"", "")); + + } else { + createOptions.add("-v " + VERSION.toString()); + } + if (DSE_ENABLEMENT) { + createOptions.add("--dse"); + } + execute( + "create", + "ccm_1", + "-i", + ipPrefix, + "-n", + Arrays.stream(nodes).mapToObj(n -> "" + n).collect(Collectors.joining(":")), + createOptions.stream().collect(Collectors.joining(" "))); + + for (Map.Entry conf : cassandraConfiguration.entrySet()) { + execute("updateconf", String.format("%s:%s", conf.getKey(), conf.getValue())); + } + if (getCassandraVersion().compareTo(Version.V2_2_0) >= 0) { + execute("updateconf", "enable_user_defined_functions:true"); + } + if (DSE_ENABLEMENT) { + for (Map.Entry conf : dseConfiguration.entrySet()) { + execute("updatedseconf", String.format("%s:%s", conf.getKey(), conf.getValue())); + } + for (String yaml : rawDseYaml) { + executeUnsanitized("updatedseconf", "-y", yaml); + } + if (!dseWorkloads.isEmpty()) { + execute("setworkload", String.join(",", dseWorkloads)); + } + } + } + } + + public void dsetool(int node, String... args) { + execute(String.format("node%d dsetool %s", node, Joiner.on(" ").join(args))); + } + + public void reloadCore(int node, String keyspace, String table, boolean reindex) { + dsetool(node, "reload_core", keyspace + "." + table, "reindex=" + reindex); + } + + public void start() { + if (started.compareAndSet(false, true)) { + execute("start", jvmArgs, "--wait-for-binary-proto"); + } + } + + public void stop() { + if (started.compareAndSet(true, false)) { + execute("stop"); + } + } + + public void remove() { + execute("remove"); + } + + public void pause(int n) { + execute("node" + n, "pause"); + } + + public void resume(int n) { + execute("node" + n, "resume"); + } + + public void start(int n) { + execute("node" + n, "start"); + } + + public void stop(int n) { + execute("node" + n, "stop"); + } + + synchronized void execute(String... args) { + String command = + "ccm " + + String.join(" ", args) + + " --config-dir=" + + configDirectory.toFile().getAbsolutePath(); + + execute(CommandLine.parse(command)); + } + + synchronized void executeUnsanitized(String... args) { + String command = "ccm "; + + CommandLine cli = CommandLine.parse(command); + for (String arg : args) { + cli.addArgument(arg, false); + } + cli.addArgument("--config-dir=" + configDirectory.toFile().getAbsolutePath()); + + execute(cli); + } + + private void execute(CommandLine cli) { + logger.debug("Executing: " + cli); + ExecuteWatchdog watchDog = new ExecuteWatchdog(TimeUnit.MINUTES.toMillis(10)); + try (LogOutputStream outStream = + new LogOutputStream() { + @Override + protected void processLine(String line, int logLevel) { + logger.debug("ccmout> {}", line); + } + }; + LogOutputStream errStream = + new LogOutputStream() { + @Override + protected void processLine(String line, int logLevel) { + logger.error("ccmerr> {}", line); + } + }) { + Executor executor = new DefaultExecutor(); + ExecuteStreamHandler streamHandler = new PumpStreamHandler(outStream, errStream); + executor.setStreamHandler(streamHandler); + executor.setWatchdog(watchDog); + + int retValue = executor.execute(cli); + if (retValue != 0) { + logger.error( + "Non-zero exit code ({}) returned from executing ccm command: {}", retValue, cli); + } + } catch (IOException ex) { + if (watchDog.killedProcess()) { + throw new RuntimeException("The command '" + cli + "' was killed after 10 minutes"); + } else { + throw new RuntimeException("The command '" + cli + "' failed to execute", ex); + } + } + } + + @Override + public void close() { + remove(); + } + + /** + * Extracts a keystore from the classpath into a temporary file. + * + *

This is needed as the keystore could be part of a built test jar used by other projects, and + * they need to be extracted to a file system so cassandra may use them. + * + * @param storePath Path in classpath where the keystore exists. + * @return The generated File. + */ + private static File createTempStore(String storePath) { + File f = null; + try (OutputStream os = new FileOutputStream(f = File.createTempFile("server", ".store"))) { + f.deleteOnExit(); + Resources.copy(CcmBridge.class.getResource(storePath), os); + } catch (IOException e) { + logger.warn("Failure to write keystore, SSL-enabled servers may fail to start.", e); + } + return f; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private int[] nodes = {1}; + private final Map cassandraConfiguration = new LinkedHashMap<>(); + private final Map dseConfiguration = new LinkedHashMap<>(); + private final List dseRawYaml = new ArrayList<>(); + private final List jvmArgs = new ArrayList<>(); + private String ipPrefix = "127.0.0."; + private final List createOptions = new ArrayList<>(); + private final List dseWorkloads = new ArrayList<>(); + + private final Path configDirectory; + + private Builder() { + try { + this.configDirectory = Files.createTempDirectory("ccm"); + // mark the ccm temp directories for deletion when the JVM exits + this.configDirectory.toFile().deleteOnExit(); + } catch (IOException e) { + // change to unchecked for now. + throw new RuntimeException(e); + } + // disable auto_snapshot by default to reduce disk usage when destroying schema. + withCassandraConfiguration("auto_snapshot", "false"); + } + + public Builder withCassandraConfiguration(String key, Object value) { + cassandraConfiguration.put(key, value); + return this; + } + + public Builder withDseConfiguration(String key, Object value) { + dseConfiguration.put(key, value); + return this; + } + + public Builder withDseConfiguration(String rawYaml) { + dseRawYaml.add(rawYaml); + return this; + } + + public Builder withJvmArgs(String... jvmArgs) { + Collections.addAll(this.jvmArgs, jvmArgs); + return this; + } + + public Builder withNodes(int... nodes) { + this.nodes = nodes; + return this; + } + + public Builder withIpPrefix(String ipPrefix) { + this.ipPrefix = ipPrefix; + return this; + } + + /** Adds an option to the {@code ccm create} command. */ + public Builder withCreateOption(String option) { + this.createOptions.add(option); + return this; + } + + /** Enables SSL encryption. */ + public Builder withSsl() { + cassandraConfiguration.put("client_encryption_options.enabled", "true"); + cassandraConfiguration.put( + "client_encryption_options.keystore", DEFAULT_SERVER_KEYSTORE_FILE.getAbsolutePath()); + cassandraConfiguration.put( + "client_encryption_options.keystore_password", DEFAULT_SERVER_KEYSTORE_PASSWORD); + return this; + } + + public Builder withSslLocalhostCn() { + cassandraConfiguration.put("client_encryption_options.enabled", "true"); + cassandraConfiguration.put( + "client_encryption_options.keystore", + DEFAULT_SERVER_LOCALHOST_KEYSTORE_FILE.getAbsolutePath()); + cassandraConfiguration.put( + "client_encryption_options.keystore_password", DEFAULT_SERVER_KEYSTORE_PASSWORD); + return this; + } + + /** Enables client authentication. This also enables encryption ({@link #withSsl()}. */ + public Builder withSslAuth() { + withSsl(); + cassandraConfiguration.put("client_encryption_options.require_client_auth", "true"); + cassandraConfiguration.put( + "client_encryption_options.truststore", DEFAULT_SERVER_TRUSTSTORE_FILE.getAbsolutePath()); + cassandraConfiguration.put( + "client_encryption_options.truststore_password", DEFAULT_SERVER_TRUSTSTORE_PASSWORD); + return this; + } + + public Builder withDseWorkloads(String... workloads) { + this.dseWorkloads.addAll(Arrays.asList(workloads)); + return this; + } + + public CcmBridge build() { + return new CcmBridge( + configDirectory, + nodes, + ipPrefix, + cassandraConfiguration, + dseConfiguration, + dseRawYaml, + createOptions, + jvmArgs, + dseWorkloads); + } + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmRule.java new file mode 100644 index 00000000000..eb12b6969e2 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CcmRule.java @@ -0,0 +1,107 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.ccm; + +import com.datastax.oss.driver.categories.ParallelizableTests; +import java.lang.reflect.Method; +import org.junit.AssumptionViolatedException; +import org.junit.experimental.categories.Category; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A rule that creates a globally shared single node Ccm cluster that is only shut down after the + * JVM exists. + * + *

Note that this rule should be considered mutually exclusive with {@link CustomCcmRule}. + * Creating instances of these rules can create resource issues. + */ +public class CcmRule extends BaseCcmRule { + + private static final CcmRule INSTANCE = new CcmRule(); + + private volatile boolean started = false; + + private CcmRule() { + super(configureCcmBridge(CcmBridge.builder()).build()); + } + + public static CcmBridge.Builder configureCcmBridge(CcmBridge.Builder builder) { + Logger logger = LoggerFactory.getLogger(CcmRule.class); + String customizerClass = + System.getProperty( + "ccmrule.bridgecustomizer", + "com.datastax.oss.driver.api.testinfra.ccm.DefaultCcmBridgeBuilderCustomizer"); + try { + Class clazz = Class.forName(customizerClass); + Method method = clazz.getMethod("configureBuilder", CcmBridge.Builder.class); + return (CcmBridge.Builder) method.invoke(null, builder); + } catch (Exception e) { + logger.warn( + "Could not find CcmRule customizer {}, will use the default CcmBridge.", + customizerClass, + e); + return builder; + } + } + + @Override + protected synchronized void before() { + if (!started) { + // synchronize before so blocks on other before() call waiting to finish. + super.before(); + started = true; + } + } + + @Override + protected void after() { + // override after so we don't remove when done. + } + + @Override + public Statement apply(Statement base, Description description) { + + Category categoryAnnotation = description.getTestClass().getAnnotation(Category.class); + if (categoryAnnotation == null + || categoryAnnotation.value().length != 1 + || categoryAnnotation.value()[0] != ParallelizableTests.class) { + return new Statement() { + @Override + public void evaluate() { + throw new AssumptionViolatedException( + String.format( + "Tests using %s must be annotated with `@Category(%s.class)`. Description: %s", + CcmRule.class.getSimpleName(), + ParallelizableTests.class.getSimpleName(), + description)); + } + }; + } + + return super.apply(base, description); + } + + public void reloadCore(int node, String keyspace, String table, boolean reindex) { + ccmBridge.reloadCore(node, keyspace, table, reindex); + } + + public static CcmRule getInstance() { + return INSTANCE; + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java new file mode 100644 index 00000000000..1e502238e99 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/CustomCcmRule.java @@ -0,0 +1,119 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.ccm; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * A rule that creates a ccm cluster that can be used in a test. This should be used if you plan on + * creating clusters with unique configurations, such as using multiple nodes, authentication, ssl + * and so on. If you do not plan on doing this at all in your tests, consider using {@link CcmRule} + * which creates a global single node CCM cluster that may be shared among tests. + * + *

Note that this rule should be considered mutually exclusive with {@link CcmRule}. Creating + * instances of these rules can create resource issues. + */ +public class CustomCcmRule extends BaseCcmRule { + + private static AtomicReference current = new AtomicReference<>(); + + CustomCcmRule(CcmBridge ccmBridge) { + super(ccmBridge); + } + + @Override + protected void before() { + if (current.get() == null && current.compareAndSet(null, this)) { + super.before(); + } else if (current.get() != this) { + throw new IllegalStateException( + "Attempting to use a Ccm rule while another is in use. This is disallowed"); + } + } + + @Override + protected void after() { + super.after(); + current.compareAndSet(this, null); + } + + public CcmBridge getCcmBridge() { + return ccmBridge; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private final CcmBridge.Builder bridgeBuilder = CcmBridge.builder(); + + public Builder withNodes(int... nodes) { + bridgeBuilder.withNodes(nodes); + return this; + } + + public Builder withCassandraConfiguration(String key, Object value) { + bridgeBuilder.withCassandraConfiguration(key, value); + return this; + } + + public Builder withDseConfiguration(String key, Object value) { + bridgeBuilder.withDseConfiguration(key, value); + return this; + } + + public Builder withDseConfiguration(String rawYaml) { + bridgeBuilder.withDseConfiguration(rawYaml); + return this; + } + + public Builder withDseWorkloads(String... workloads) { + bridgeBuilder.withDseWorkloads(workloads); + return this; + } + + public Builder withJvmArgs(String... jvmArgs) { + bridgeBuilder.withJvmArgs(jvmArgs); + return this; + } + + public Builder withCreateOption(String option) { + bridgeBuilder.withCreateOption(option); + return this; + } + + public Builder withSsl() { + bridgeBuilder.withSsl(); + return this; + } + + public Builder withSslLocalhostCn() { + bridgeBuilder.withSslLocalhostCn(); + return this; + } + + public Builder withSslAuth() { + bridgeBuilder.withSslAuth(); + return this; + } + + public CustomCcmRule build() { + return new CustomCcmRule(bridgeBuilder.build()); + } + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/DefaultCcmBridgeBuilderCustomizer.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/DefaultCcmBridgeBuilderCustomizer.java new file mode 100644 index 00000000000..01cf3888aa2 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/ccm/DefaultCcmBridgeBuilderCustomizer.java @@ -0,0 +1,28 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.ccm; + +import com.datastax.oss.driver.api.core.Version; + +public class DefaultCcmBridgeBuilderCustomizer { + + public static CcmBridge.Builder configureBuilder(CcmBridge.Builder builder) { + if (!CcmBridge.DSE_ENABLEMENT && CcmBridge.VERSION.compareTo(Version.V4_0_0) >= 0) { + builder.withCassandraConfiguration("enable_materialized_views", true); + } + return builder; + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/loadbalancing/SortingLoadBalancingPolicy.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/loadbalancing/SortingLoadBalancingPolicy.java new file mode 100644 index 00000000000..678b1477dee --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/loadbalancing/SortingLoadBalancingPolicy.java @@ -0,0 +1,112 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.loadbalancing; + +import com.datastax.oss.driver.api.core.context.DriverContext; +import com.datastax.oss.driver.api.core.loadbalancing.LoadBalancingPolicy; +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.session.Request; +import com.datastax.oss.driver.api.core.session.Session; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.ArrayDeque; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; + +public class SortingLoadBalancingPolicy implements LoadBalancingPolicy { + + @SuppressWarnings("unused") + public SortingLoadBalancingPolicy(DriverContext context, String profileName) { + // constructor needed for loading via config. + } + + private byte[] empty = {}; + private final Set nodes = + new TreeSet<>( + (node1, node2) -> { + // compare address bytes, byte by byte. + byte[] address1 = + node1 + .getBroadcastAddress() + .map(InetSocketAddress::getAddress) + .map(InetAddress::getAddress) + .orElse(empty); + byte[] address2 = + node2 + .getBroadcastAddress() + .map(InetSocketAddress::getAddress) + .map(InetAddress::getAddress) + .orElse(empty); + + // ipv6 vs ipv4, favor ipv6. + if (address1.length != address2.length) { + return address1.length - address2.length; + } + + for (int i = 0; i < address1.length; i++) { + int b1 = address1[i] & 0xFF; + int b2 = address2[i] & 0xFF; + if (b1 != b2) { + return b1 - b2; + } + } + int port1 = node1.getBroadcastAddress().map(InetSocketAddress::getPort).orElse(0); + int port2 = node2.getBroadcastAddress().map(InetSocketAddress::getPort).orElse(0); + return port1 - port2; + }); + + public SortingLoadBalancingPolicy() {} + + @Override + public void init(@NonNull Map nodes, @NonNull DistanceReporter distanceReporter) { + this.nodes.addAll(nodes.values()); + this.nodes.forEach(n -> distanceReporter.setDistance(n, NodeDistance.LOCAL)); + } + + @NonNull + @Override + public Queue newQueryPlan(@NonNull Request request, @NonNull Session session) { + return new ArrayDeque<>(nodes); + } + + @Override + public void onAdd(@NonNull Node node) { + this.nodes.add(node); + } + + @Override + public void onUp(@NonNull Node node) { + onAdd(node); + } + + @Override + public void onDown(@NonNull Node node) { + onRemove(node); + } + + @Override + public void onRemove(@NonNull Node node) { + this.nodes.remove(node); + } + + @Override + public void close() {} +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/CqlSessionRuleBuilder.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/CqlSessionRuleBuilder.java new file mode 100644 index 00000000000..3d864ab007c --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/CqlSessionRuleBuilder.java @@ -0,0 +1,32 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.session; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.testinfra.CassandraResourceRule; + +public class CqlSessionRuleBuilder extends SessionRuleBuilder { + + public CqlSessionRuleBuilder(CassandraResourceRule cassandraResource) { + super(cassandraResource); + } + + @Override + public SessionRule build() { + return new SessionRule<>( + cassandraResource, createKeyspace, nodeStateListener, schemaChangeListener, loader); + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionRule.java new file mode 100644 index 00000000000..585aaff2aa7 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionRule.java @@ -0,0 +1,135 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.session; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.testinfra.CassandraResourceRule; +import com.datastax.oss.driver.api.testinfra.simulacron.SimulacronRule; +import org.junit.rules.ExternalResource; + +/** + * Creates and manages a {@link Session} instance for a test. + * + *

Use it in conjunction with a {@link CassandraResourceRule} that creates the server resource to + * connect to: + * + *

{@code
+ * public static @ClassRule CcmRule server = CcmRule.getInstance();
+ *
+ * // Or: public static @ClassRule SimulacronRule server =
+ * //    new SimulacronRule(ClusterSpec.builder().withNodes(3));
+ *
+ * public static @ClassRule SessionRule sessionRule = new SessionRule(server);
+ *
+ * public void @Test should_do_something() {
+ *   sessionRule.session().execute("some query");
+ * }
+ * }
+ * + * Optionally, it can also create a dedicated keyspace (useful to isolate tests that share a common + * server). + * + *

If you would rather create a new keyspace manually in each test, see the utility methods in + * {@link SessionUtils}. + */ +public class SessionRule extends ExternalResource { + + // the CCM or Simulacron rule to depend on + private final CassandraResourceRule cassandraResource; + private final NodeStateListener nodeStateListener; + private final SchemaChangeListener schemaChangeListener; + private final CqlIdentifier keyspace; + private final DriverConfigLoader configLoader; + + // the session that is auto created for this rule and is tied to the given keyspace. + private SessionT session; + + private DriverExecutionProfile slowProfile; + + /** + * Returns a builder to construct an instance with a fluent API. + * + * @param cassandraResource resource to create clusters for. + */ + public static CqlSessionRuleBuilder builder(CassandraResourceRule cassandraResource) { + return new CqlSessionRuleBuilder(cassandraResource); + } + + /** @see #builder(CassandraResourceRule) */ + public SessionRule( + CassandraResourceRule cassandraResource, + boolean createKeyspace, + NodeStateListener nodeStateListener, + SchemaChangeListener schemaChangeListener, + DriverConfigLoader configLoader) { + this.cassandraResource = cassandraResource; + this.nodeStateListener = nodeStateListener; + this.schemaChangeListener = schemaChangeListener; + this.keyspace = + (cassandraResource instanceof SimulacronRule || !createKeyspace) + ? null + : SessionUtils.uniqueKeyspaceId(); + this.configLoader = configLoader; + } + + @Override + protected void before() { + session = + SessionUtils.newSession( + cassandraResource, null, nodeStateListener, schemaChangeListener, null, configLoader); + slowProfile = SessionUtils.slowProfile(session); + if (keyspace != null) { + SessionUtils.createKeyspace(session, keyspace, slowProfile); + session.execute( + SimpleStatement.newInstance(String.format("USE %s", keyspace.asCql(false))), + Statement.SYNC); + } + } + + @Override + protected void after() { + if (keyspace != null) { + SessionUtils.dropKeyspace(session, keyspace, slowProfile); + } + session.close(); + } + + /** @return the session created with this rule. */ + public SessionT session() { + return session; + } + + /** + * @return the identifier of the keyspace associated with this rule, or {@code null} if no + * keyspace was created (this is always the case if the server resource is a {@link + * SimulacronRule}). + */ + public CqlIdentifier keyspace() { + return keyspace; + } + + /** @return a config profile where the request timeout is 30 seconds. * */ + public DriverExecutionProfile slowProfile() { + return slowProfile; + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionRuleBuilder.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionRuleBuilder.java new file mode 100644 index 00000000000..5bc56b0c4a2 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionRuleBuilder.java @@ -0,0 +1,78 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.session; + +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.testinfra.CassandraResourceRule; +import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; +import com.datastax.oss.driver.api.testinfra.simulacron.SimulacronRule; + +public abstract class SessionRuleBuilder< + SelfT extends SessionRuleBuilder, SessionT extends Session> { + + protected final CassandraResourceRule cassandraResource; + protected boolean createKeyspace = true; + protected NodeStateListener nodeStateListener; + protected SchemaChangeListener schemaChangeListener; + protected DriverConfigLoader loader; + + @SuppressWarnings("unchecked") + protected final SelfT self = (SelfT) this; + + public SessionRuleBuilder(CassandraResourceRule cassandraResource) { + this.cassandraResource = cassandraResource; + } + + /** + * Whether to create a keyspace. + * + *

If this is set, the rule will create a keyspace with a name unique to this test (this allows + * multiple tests to run concurrently against the same server resource), and make the name + * available through {@link SessionRule#keyspace()}. The created session will be connected to this + * keyspace. + * + *

If this method is not called, the default value is {@code true}. + * + *

Note that this option is only valid with a {@link CcmRule}. If the server resource is a + * {@link SimulacronRule}, this option is ignored, no keyspace gets created, and {@link + * SessionRule#keyspace()} returns {@code null}. + */ + public SelfT withKeyspace(boolean createKeyspace) { + this.createKeyspace = createKeyspace; + return self; + } + + /** A set of options to override in the session configuration. */ + public SelfT withConfigLoader(DriverConfigLoader loader) { + this.loader = loader; + return self; + } + + public SelfT withNodeStateListener(NodeStateListener listener) { + this.nodeStateListener = listener; + return self; + } + + public SelfT withSchemaChangeListener(SchemaChangeListener listener) { + this.schemaChangeListener = listener; + return self; + } + + public abstract SessionRule build(); +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionUtils.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionUtils.java new file mode 100644 index 00000000000..139631f26c3 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/session/SessionUtils.java @@ -0,0 +1,245 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.session; + +import com.datastax.oss.driver.api.core.CqlIdentifier; +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.config.DefaultDriverOption; +import com.datastax.oss.driver.api.core.config.DriverConfigLoader; +import com.datastax.oss.driver.api.core.config.DriverExecutionProfile; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; +import com.datastax.oss.driver.api.core.cql.Statement; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeStateListener; +import com.datastax.oss.driver.api.core.metadata.schema.SchemaChangeListener; +import com.datastax.oss.driver.api.core.session.Session; +import com.datastax.oss.driver.api.core.session.SessionBuilder; +import com.datastax.oss.driver.api.testinfra.CassandraResourceRule; +import com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoader; +import com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoaderBuilder; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility methods to manage {@link Session} instances manually. + * + *

Use this if you need to initialize a new session instance in each test method: + * + *

{@code
+ * public static @ClassRule CcmRule server = CcmRule.getInstance();
+ *
+ * // Or: public static @ClassRule SimulacronRule server =
+ * //    new SimulacronRule(ClusterSpec.builder().withNodes(3));
+ *
+ * public void @Test should_do_something() {
+ *   try (Session session = TestUtils.newSession(server)) {
+ *     session.execute("some query");
+ *   }
+ * }
+ * }
+ * + * The instances returned by {@code newSession()} methods are not managed automatically, you need to + * close them yourself (this is done with a try-with-resources block in the example above). + * + *

If you can share the same {@code Session} instance between all test methods, {@link + * SessionRule} provides a simpler alternative. + */ +public class SessionUtils { + private static final Logger LOG = LoggerFactory.getLogger(SessionUtils.class); + private static final AtomicInteger keyspaceId = new AtomicInteger(); + private static final String DEFAULT_SESSION_CLASS_NAME = CqlSession.class.getName(); + private static final String SESSION_BUILDER_CLASS = + System.getProperty("session.builder", DEFAULT_SESSION_CLASS_NAME); + + @SuppressWarnings("unchecked") + public static SessionBuilder baseBuilder() { + try { + Class clazz = Class.forName(SESSION_BUILDER_CLASS); + Method m = clazz.getMethod("builder"); + return (SessionBuilder) m.invoke(null); + } catch (Exception e) { + LOG.warn( + "Could not construct SessionBuilder from {} using builder(), using default " + + "implementation.", + SESSION_BUILDER_CLASS, + e); + return (SessionBuilder) CqlSession.builder(); + } + } + + /** @leaks-private-api Tests use this for programmatic config loading. */ + public static DefaultDriverConfigLoaderBuilder configLoaderBuilder() { + try { + Class clazz = Class.forName(SESSION_BUILDER_CLASS); + Method m = clazz.getMethod("configLoaderBuilder"); + return (DefaultDriverConfigLoaderBuilder) m.invoke(null); + } catch (Exception e) { + if (!SESSION_BUILDER_CLASS.equals(DEFAULT_SESSION_CLASS_NAME)) { + LOG.warn( + "Could not construct DefaultDriverConfigLoaderBuilder from {} using " + + "configLoaderBuilder(), using default implementation.", + SESSION_BUILDER_CLASS, + e); + } + return DefaultDriverConfigLoader.builder(); + } + } + + /** + * Creates a new instance of the driver's default {@code Session} implementation, using the nodes + * in the 0th DC of the provided Cassandra resource as contact points, and the default + * configuration augmented with the provided options. + */ + @SuppressWarnings("TypeParameterUnusedInFormals") + public static SessionT newSession( + CassandraResourceRule cassandraResource) { + return newSession(cassandraResource, null, null); + } + + @SuppressWarnings("TypeParameterUnusedInFormals") + public static SessionT newSession( + CassandraResourceRule cassandraResource, CqlIdentifier keyspace) { + return newSession(cassandraResource, keyspace, null, null, null); + } + + @SuppressWarnings("TypeParameterUnusedInFormals") + public static SessionT newSession( + CassandraResourceRule cassandraResourceRule, DriverConfigLoader loader) { + return newSession(cassandraResourceRule, null, null, null, null, loader); + } + + @SuppressWarnings("TypeParameterUnusedInFormals") + public static SessionT newSession( + CassandraResourceRule cassandraResourceRule, + CqlIdentifier keyspace, + DriverConfigLoader loader) { + return newSession(cassandraResourceRule, keyspace, null, null, null, loader); + } + + private static SessionBuilder builder( + CassandraResourceRule cassandraResource, + CqlIdentifier keyspace, + NodeStateListener nodeStateListener, + SchemaChangeListener schemaChangeListener, + Predicate nodeFilter) { + SessionBuilder builder = + baseBuilder() + .addContactEndPoints(cassandraResource.getContactPoints()) + .withKeyspace(keyspace) + .withNodeStateListener(nodeStateListener) + .withSchemaChangeListener(schemaChangeListener); + if (nodeFilter != null) { + builder = builder.withNodeFilter(nodeFilter); + } + return builder; + } + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + public static SessionT newSession( + CassandraResourceRule cassandraResource, + CqlIdentifier keyspace, + NodeStateListener nodeStateListener, + SchemaChangeListener schemaChangeListener, + Predicate nodeFilter) { + SessionBuilder builder = + builder(cassandraResource, keyspace, nodeStateListener, schemaChangeListener, nodeFilter); + return (SessionT) builder.build(); + } + + @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) + public static SessionT newSession( + CassandraResourceRule cassandraResource, + CqlIdentifier keyspace, + NodeStateListener nodeStateListener, + SchemaChangeListener schemaChangeListener, + Predicate nodeFilter, + DriverConfigLoader loader) { + SessionBuilder builder = + builder(cassandraResource, keyspace, nodeStateListener, schemaChangeListener, nodeFilter); + return (SessionT) builder.withConfigLoader(loader).build(); + } + + /** + * Generates a keyspace identifier that is guaranteed to be unique in the current classloader. + * + *

This is useful to isolate tests that share a common server resource. + */ + public static CqlIdentifier uniqueKeyspaceId() { + return CqlIdentifier.fromCql("ks_" + keyspaceId.getAndIncrement()); + } + + /** Creates a keyspace through the given session instance, with the given profile. */ + public static void createKeyspace( + Session session, CqlIdentifier keyspace, DriverExecutionProfile profile) { + SimpleStatement createKeyspace = + SimpleStatement.builder( + String.format( + "CREATE KEYSPACE %s WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 1 };", + keyspace.asCql(false))) + .setExecutionProfile(profile) + .build(); + session.execute(createKeyspace, Statement.SYNC); + } + + /** + * Calls {@link #createKeyspace(Session, CqlIdentifier, DriverExecutionProfile)} with {@link + * #slowProfile(Session)} as the third argument. + * + *

Note that this creates a derived profile for each invocation, which has a slight performance + * overhead. Instead, consider building the profile manually with {@link #slowProfile(Session)}, + * and storing it in a local variable so it can be reused. + */ + public static void createKeyspace(Session session, CqlIdentifier keyspace) { + createKeyspace(session, keyspace, slowProfile(session)); + } + + /** Drops a keyspace through the given session instance, with the given profile. */ + public static void dropKeyspace( + Session session, CqlIdentifier keyspace, DriverExecutionProfile profile) { + session.execute( + SimpleStatement.builder(String.format("DROP KEYSPACE IF EXISTS %s", keyspace.asCql(false))) + .setExecutionProfile(profile) + .build(), + Statement.SYNC); + } + + /** + * Calls {@link #dropKeyspace(Session, CqlIdentifier, DriverExecutionProfile)} with {@link + * #slowProfile(Session)} as the third argument. + * + *

Note that this creates a derived profile for each invocation, which has a slight performance + * overhead. Instead, consider building the profile manually with {@link #slowProfile(Session)}, + * and storing it in a local variable so it can be reused. + */ + public static void dropKeyspace(Session session, CqlIdentifier keyspace) { + dropKeyspace(session, keyspace, slowProfile(session)); + } + + /** + * Builds a profile derived from the given cluster's default profile, with a higher request + * timeout (30 seconds) that is appropriate for DML operations. + */ + public static DriverExecutionProfile slowProfile(Session session) { + return session + .getContext() + .getConfig() + .getDefaultProfile() + .withString(DefaultDriverOption.REQUEST_TIMEOUT, "30s"); + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/simulacron/QueryCounter.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/simulacron/QueryCounter.java new file mode 100644 index 00000000000..ffca180c13f --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/simulacron/QueryCounter.java @@ -0,0 +1,157 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.simulacron; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.testinfra.utils.ConditionChecker; +import com.datastax.oss.simulacron.common.cluster.QueryLog; +import com.datastax.oss.simulacron.server.BoundNode; +import com.datastax.oss.simulacron.server.BoundTopic; +import com.datastax.oss.simulacron.server.listener.QueryListener; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +/** + * A convenience utility that keeps track of the number of queries matching a filter received by + * {@link BoundNode}s. + * + *

One tricky thing about validating query counts in context of testing is in cases where you + * don't require waiting for a node to respond. In this case it's possible that a user would check + * count criteria before the node has even processed the message. This class offers the capability + * to wait a specified amount of time when asserting query arrival counts on nodes. + */ +public class QueryCounter { + + private final long beforeTimeout; + private final TimeUnit beforeUnit; + private final AtomicInteger totalCount = new AtomicInteger(0); + private final ConcurrentHashMap countMap = new ConcurrentHashMap<>(); + + public enum NotificationMode { + BEFORE_PROCESSING, + AFTER_PROCESSING + } + + private QueryCounter( + BoundTopic topic, + NotificationMode notificationMode, + Predicate queryLogFilter, + long beforeTimeout, + TimeUnit beforeUnit) { + this.beforeTimeout = beforeTimeout; + this.beforeUnit = beforeUnit; + QueryListener listener = + (boundNode, queryLog) -> { + totalCount.incrementAndGet(); + countMap.merge(boundNode.getId().intValue(), 1, Integer::sum); + }; + topic.registerQueryListener( + listener, notificationMode == NotificationMode.AFTER_PROCESSING, queryLogFilter); + } + + /** Creates a builder that tracks counts for the given {@link BoundTopic} (cluster, dc, node). */ + public static QueryCounterBuilder builder(BoundTopic topic) { + return new QueryCounterBuilder(topic); + } + + /** Clears all counters. */ + public void clearCounts() { + totalCount.set(0); + countMap.clear(); + } + + /** + * Asserts that the total number of requests received matching filter criteria matches the + * expected count within the configured time period. + */ + public void assertTotalCount(int expected) { + ConditionChecker.checkThat(() -> assertThat(totalCount.get()).isEqualTo(expected)) + .every(10, TimeUnit.MILLISECONDS) + .before(beforeTimeout, beforeUnit) + .becomesTrue(); + } + + /** + * Asserts that the total number of requests received matcher filter criteria matches the expected + * count for each node within the configured time period. + * + * @param counts The expected node counts, with the value at each index matching the expected + * count for that node id (i.e. index 0 = node id 0 expected count). + */ + public void assertNodeCounts(int... counts) { + Map expectedCounts = new HashMap<>(); + for (int id = 0; id < counts.length; id++) { + int count = counts[id]; + if (count > 0) { + expectedCounts.put(id, counts[id]); + } + } + ConditionChecker.checkThat(() -> assertThat(countMap).containsAllEntriesOf(expectedCounts)) + .every(10, TimeUnit.MILLISECONDS) + .before(beforeTimeout, beforeUnit) + .becomesTrue(); + } + + public static class QueryCounterBuilder { + + @SuppressWarnings("deprecation") + private static Predicate DEFAULT_FILTER = (q) -> !q.getQuery().isEmpty(); + + private Predicate queryLogFilter = DEFAULT_FILTER; + private BoundTopic topic; + private NotificationMode notificationMode = NotificationMode.BEFORE_PROCESSING; + private long beforeTimeout = 1; + private TimeUnit beforeUnit = TimeUnit.SECONDS; + + private QueryCounterBuilder(BoundTopic topic) { + this.topic = topic; + } + + /** + * The filter to apply to consider a message received by the node, if not provided we consider + * all messages that are queries. + */ + public QueryCounterBuilder withFilter(Predicate queryLogFilter) { + this.queryLogFilter = queryLogFilter; + return this; + } + + /** Whether or not simulacron should notify before or after the message is processed. */ + public QueryCounterBuilder withNotification(NotificationMode notificationMode) { + this.notificationMode = notificationMode; + return this; + } + + /** + * Up to how long we check counts to match. If counts don't match after this time, an exception + * is thrown. + */ + public QueryCounterBuilder before(long timeout, TimeUnit unit) { + this.beforeTimeout = timeout; + this.beforeUnit = unit; + return this; + } + + public QueryCounter build() { + return new QueryCounter(topic, notificationMode, queryLogFilter, beforeTimeout, beforeUnit); + } + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/simulacron/SimulacronRule.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/simulacron/SimulacronRule.java new file mode 100644 index 00000000000..c15a257a781 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/simulacron/SimulacronRule.java @@ -0,0 +1,98 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.simulacron; + +import com.datastax.oss.driver.api.core.DefaultProtocolVersion; +import com.datastax.oss.driver.api.core.ProtocolVersion; +import com.datastax.oss.driver.api.core.metadata.EndPoint; +import com.datastax.oss.driver.api.testinfra.CassandraResourceRule; +import com.datastax.oss.driver.internal.core.metadata.DefaultEndPoint; +import com.datastax.oss.simulacron.common.cluster.ClusterSpec; +import com.datastax.oss.simulacron.server.BoundCluster; +import com.datastax.oss.simulacron.server.Inet4Resolver; +import com.datastax.oss.simulacron.server.Server; +import java.net.SocketAddress; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +public class SimulacronRule extends CassandraResourceRule { + // TODO perhaps share server some other way + // TODO: Temporarily do not release addresses to ensure IPs are always ordered + public static final Server server = + Server.builder() + .withAddressResolver( + new Inet4Resolver(9043) { + @Override + public void release(SocketAddress address) {} + }) + .build(); + + private final ClusterSpec clusterSpec; + private BoundCluster boundCluster; + + private final AtomicBoolean started = new AtomicBoolean(); + + public SimulacronRule(ClusterSpec clusterSpec) { + this.clusterSpec = clusterSpec; + } + + public SimulacronRule(ClusterSpec.Builder clusterSpec) { + this(clusterSpec.build()); + } + + /** + * Convenient fluent name for getting at bound cluster. + * + * @return default bound cluster for this simulacron instance. + */ + public BoundCluster cluster() { + return boundCluster; + } + + public BoundCluster getBoundCluster() { + return boundCluster; + } + + @Override + protected void before() { + // prevent duplicate initialization of rule + if (started.compareAndSet(false, true)) { + boundCluster = server.register(clusterSpec); + } + } + + @Override + protected void after() { + boundCluster.close(); + } + + /** @return All nodes in first data center. */ + @Override + public Set getContactPoints() { + return boundCluster + .dc(0) + .getNodes() + .stream() + .map(node -> new DefaultEndPoint(node.inetSocketAddress())) + .collect(Collectors.toSet()); + } + + @Override + public ProtocolVersion getHighestProtocolVersion() { + return DefaultProtocolVersion.V4; + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/utils/ConditionChecker.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/utils/ConditionChecker.java new file mode 100644 index 00000000000..e57cf1fedbd --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/utils/ConditionChecker.java @@ -0,0 +1,192 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.utils; + +import static org.assertj.core.api.Fail.fail; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BooleanSupplier; + +public class ConditionChecker { + + private static final int DEFAULT_PERIOD_MILLIS = 500; + + private static final int DEFAULT_TIMEOUT_MILLIS = 60000; + + public static class ConditionCheckerBuilder { + + private long timeout = DEFAULT_TIMEOUT_MILLIS; + + private TimeUnit timeoutUnit = TimeUnit.MILLISECONDS; + + private long period = DEFAULT_PERIOD_MILLIS; + + private TimeUnit periodUnit = TimeUnit.MILLISECONDS; + + private final Object predicate; + + private String description; + + ConditionCheckerBuilder(BooleanSupplier predicate) { + this.predicate = predicate; + } + + public ConditionCheckerBuilder(Runnable predicate) { + this.predicate = predicate; + } + + public ConditionCheckerBuilder every(long period, TimeUnit unit) { + this.period = period; + periodUnit = unit; + return this; + } + + public ConditionCheckerBuilder every(long periodMillis) { + period = periodMillis; + periodUnit = TimeUnit.MILLISECONDS; + return this; + } + + public ConditionCheckerBuilder before(long timeout, TimeUnit unit) { + this.timeout = timeout; + timeoutUnit = unit; + return this; + } + + public ConditionCheckerBuilder before(long timeoutMillis) { + timeout = timeoutMillis; + timeoutUnit = TimeUnit.MILLISECONDS; + return this; + } + + public ConditionCheckerBuilder as(String description) { + this.description = description; + return this; + } + + public void becomesTrue() { + new ConditionChecker(predicate, true, period, periodUnit, description) + .await(timeout, timeoutUnit); + } + + public void becomesFalse() { + new ConditionChecker(predicate, false, period, periodUnit, description) + .await(timeout, timeoutUnit); + } + } + + public static ConditionCheckerBuilder checkThat(BooleanSupplier predicate) { + return new ConditionCheckerBuilder(predicate); + } + + public static ConditionCheckerBuilder checkThat(Runnable predicate) { + return new ConditionCheckerBuilder(predicate); + } + + private final Object predicate; + private final boolean expectedOutcome; + private final String description; + private final Lock lock; + private final Condition condition; + private final Timer timer; + private Throwable lastFailure; + + public ConditionChecker( + Object predicate, + boolean expectedOutcome, + long period, + TimeUnit periodUnit, + String description) { + this.predicate = predicate; + this.expectedOutcome = expectedOutcome; + this.description = (description != null) ? description : this.toString(); + lock = new ReentrantLock(); + condition = lock.newCondition(); + timer = new Timer("condition-checker", true); + timer.schedule( + new TimerTask() { + @Override + public void run() { + checkCondition(); + } + }, + 0, + periodUnit.toMillis(period)); + } + + /** Waits until the predicate becomes true, or a timeout occurs, whichever happens first. */ + public void await(long timeout, TimeUnit unit) { + boolean interrupted = false; + long nanos = unit.toNanos(timeout); + lock.lock(); + try { + while (!evalCondition()) { + if (nanos <= 0L) { + String msg = + String.format( + "Timeout after %s %s while waiting for '%s'", + timeout, unit.toString().toLowerCase(), description); + if (lastFailure != null) { + fail(msg, lastFailure); + } else { + fail(msg); + } + } + try { + nanos = condition.awaitNanos(nanos); + } catch (InterruptedException e) { + interrupted = true; + } + } + } finally { + timer.cancel(); + if (interrupted) Thread.currentThread().interrupt(); + } + } + + private void checkCondition() { + lock.lock(); + try { + if (evalCondition()) { + condition.signal(); + } + } finally { + lock.unlock(); + } + } + + private boolean evalCondition() { + if (predicate instanceof BooleanSupplier) { + return ((BooleanSupplier) predicate).getAsBoolean() == expectedOutcome; + } else if (predicate instanceof Runnable) { + boolean succeeded = true; + try { + ((Runnable) predicate).run(); + } catch (Throwable t) { + succeeded = false; + lastFailure = t; + } + return succeeded == expectedOutcome; + } else { + throw new AssertionError("Unsupported predicate type " + predicate.getClass()); + } + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/utils/NodeUtils.java b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/utils/NodeUtils.java new file mode 100644 index 00000000000..300befe71d4 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/api/testinfra/utils/NodeUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.api.testinfra.utils; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NodeUtils { + + private static final Logger logger = LoggerFactory.getLogger(NodeUtils.class); + + private static final int TEST_BASE_NODE_WAIT = 60; + + public static void waitForUp(Node node) { + waitFor(node, TEST_BASE_NODE_WAIT, NodeState.UP); + } + + public static void waitForUp(Node node, int timeoutSeconds) { + waitFor(node, timeoutSeconds, NodeState.UP); + } + + public static void waitForDown(Node node) { + waitFor(node, TEST_BASE_NODE_WAIT * 3, NodeState.DOWN); + } + + public static void waitForDown(Node node, int timeoutSeconds) { + waitFor(node, timeoutSeconds, NodeState.DOWN); + } + + public static void waitFor(Node node, int timeoutSeconds, NodeState nodeState) { + logger.debug("Waiting for node {} to enter state {}", node, nodeState); + ConditionChecker.checkThat(() -> node.getState().equals(nodeState)) + .every(100, MILLISECONDS) + .before(timeoutSeconds, SECONDS) + .becomesTrue(); + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/assertions/Assertions.java b/test-infra/src/main/java/com/datastax/oss/driver/assertions/Assertions.java new file mode 100644 index 00000000000..1a70c5a9c94 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/assertions/Assertions.java @@ -0,0 +1,24 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.assertions; + +import com.datastax.oss.driver.api.core.metadata.Node; + +public class Assertions extends org.assertj.core.api.Assertions { + public static NodeMetadataAssert assertThat(Node actual) { + return new NodeMetadataAssert(actual); + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/assertions/NodeMetadataAssert.java b/test-infra/src/main/java/com/datastax/oss/driver/assertions/NodeMetadataAssert.java new file mode 100644 index 00000000000..904f60f9793 --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/assertions/NodeMetadataAssert.java @@ -0,0 +1,80 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.assertions; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.datastax.oss.driver.api.core.loadbalancing.NodeDistance; +import com.datastax.oss.driver.api.core.metadata.Node; +import com.datastax.oss.driver.api.core.metadata.NodeState; +import org.assertj.core.api.AbstractAssert; + +public class NodeMetadataAssert extends AbstractAssert { + + public NodeMetadataAssert(Node actual) { + super(actual, NodeMetadataAssert.class); + } + + public NodeMetadataAssert isUp() { + assertThat(actual.getState()).isSameAs(NodeState.UP); + return this; + } + + public NodeMetadataAssert isDown() { + assertThat(actual.getState()).isSameAs(NodeState.DOWN); + return this; + } + + public NodeMetadataAssert isUnknown() { + assertThat(actual.getState()).isSameAs(NodeState.UNKNOWN); + return this; + } + + public NodeMetadataAssert isForcedDown() { + assertThat(actual.getState()).isSameAs(NodeState.FORCED_DOWN); + return this; + } + + public NodeMetadataAssert hasOpenConnections(int expected) { + assertThat(actual.getOpenConnections()).isEqualTo(expected); + return this; + } + + public NodeMetadataAssert isReconnecting() { + assertThat(actual.isReconnecting()).isTrue(); + return this; + } + + public NodeMetadataAssert isNotReconnecting() { + assertThat(actual.isReconnecting()).isFalse(); + return this; + } + + public NodeMetadataAssert isLocal() { + assertThat(actual.getDistance()).isSameAs(NodeDistance.LOCAL); + return this; + } + + public NodeMetadataAssert isRemote() { + assertThat(actual.getDistance()).isSameAs(NodeDistance.REMOTE); + return this; + } + + public NodeMetadataAssert isIgnored() { + assertThat(actual.getDistance()).isSameAs(NodeDistance.IGNORED); + return this; + } +} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/categories/IsolatedTests.java b/test-infra/src/main/java/com/datastax/oss/driver/categories/IsolatedTests.java new file mode 100644 index 00000000000..4309d0c15fc --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/categories/IsolatedTests.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.categories; + +/** + * Defines a classification of tests that should be run in their own jvm fork. + * + *

This is generally because they need to set system properties. + */ +public interface IsolatedTests {} diff --git a/test-infra/src/main/java/com/datastax/oss/driver/categories/ParallelizableTests.java b/test-infra/src/main/java/com/datastax/oss/driver/categories/ParallelizableTests.java new file mode 100644 index 00000000000..1b6efa9115a --- /dev/null +++ b/test-infra/src/main/java/com/datastax/oss/driver/categories/ParallelizableTests.java @@ -0,0 +1,25 @@ +/* + * Copyright DataStax, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.datastax.oss.driver.categories; + +import com.datastax.oss.driver.api.testinfra.ccm.CcmRule; +import com.datastax.oss.driver.api.testinfra.ccm.CustomCcmRule; + +/** + * Defines a classification of tests that can be run in parallel, namely: tests that use {@link + * CcmRule} (not {@link CustomCcmRule}), and tests that use Simulacron. + */ +public interface ParallelizableTests {} diff --git a/driver-core/src/test/resources/client.crt b/test-infra/src/main/resources/client.crt similarity index 100% rename from driver-core/src/test/resources/client.crt rename to test-infra/src/main/resources/client.crt diff --git a/driver-core/src/test/resources/client.key b/test-infra/src/main/resources/client.key similarity index 100% rename from driver-core/src/test/resources/client.key rename to test-infra/src/main/resources/client.key diff --git a/driver-core/src/test/resources/client.keystore b/test-infra/src/main/resources/client.keystore similarity index 100% rename from driver-core/src/test/resources/client.keystore rename to test-infra/src/main/resources/client.keystore diff --git a/test-infra/src/main/resources/client.truststore b/test-infra/src/main/resources/client.truststore new file mode 100644 index 00000000000..f106bdee38d Binary files /dev/null and b/test-infra/src/main/resources/client.truststore differ diff --git a/driver-core/src/test/resources/server.keystore b/test-infra/src/main/resources/server.keystore similarity index 100% rename from driver-core/src/test/resources/server.keystore rename to test-infra/src/main/resources/server.keystore diff --git a/driver-core/src/test/resources/server.truststore b/test-infra/src/main/resources/server.truststore similarity index 100% rename from driver-core/src/test/resources/server.truststore rename to test-infra/src/main/resources/server.truststore diff --git a/test-infra/src/main/resources/server_localhost.keystore b/test-infra/src/main/resources/server_localhost.keystore new file mode 100644 index 00000000000..05e7a559c5d Binary files /dev/null and b/test-infra/src/main/resources/server_localhost.keystore differ diff --git a/testing/README.md b/testing/README.md deleted file mode 100644 index 4dfbb525351..00000000000 --- a/testing/README.md +++ /dev/null @@ -1,79 +0,0 @@ -## Testing Prerequisites - -### Install CCM - - pip install ccm - -### Setup CCM Loopbacks (required for OSX) - - # For basic ccm - sudo ifconfig lo0 alias 127.0.0.2 up - sudo ifconfig lo0 alias 127.0.0.3 up - - # Additional loopbacks for java-driver testing - sudo ifconfig lo0 alias 127.0.1.1 up - sudo ifconfig lo0 alias 127.0.1.2 up - sudo ifconfig lo0 alias 127.0.1.3 up - sudo ifconfig lo0 alias 127.0.1.4 up - sudo ifconfig lo0 alias 127.0.1.5 up - sudo ifconfig lo0 alias 127.0.1.6 up - - - -## Building the Driver - - mvn clean package - - - -## Testing the Driver - -### Unit Tests - -Use the following command to run only the unit tests: - - mvn test - -_**Estimated Run Time**: x minutes_ - -### Integration Tests - -The following command runs the full set of unit and integration tests: - - mvn verify - -_**Estimated Run Time**: 4 minutes_ - -### Coverage Report - -The following command runs the full set of integration tests and produces a -coverage report: - - mvn cobertura:cobertura - -Coverage report can be found at: - - driver-core/target/site/cobertura/index.html - -_**Estimated Run Time**: 4 minutes_ - - - -## Test Utility - -`testing/bin/coverage` exists to make testing a bit more straight-forward. - -The main commands are as follows: - -Displays the available parameters: - - testing/bin/coverage --help - -Runs all the integration tests, creates the Cobertura report, and uploads Cobertura -site to a remote machine, if applicable: - - testing/bin/coverage - -Runs a single integration test along with the Cobertura report for that test: - - testing/bin/coverage --test TestClass[#optionalTestMethod] diff --git a/testing/bin/coverage b/testing/bin/coverage deleted file mode 100755 index c920e9fa6d1..00000000000 --- a/testing/bin/coverage +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python - -import argparse -import ConfigParser -import datetime -import os -import platform -import shlex -import subprocess -import sys - -USER_CONFIG = '~/.java_driver_tests.conf' - -def read_config(section, option, data_type='string', default=None): - '''Read configs as stored in the above defined USER_CONFIG.''' - - config = ConfigParser.ConfigParser() - config.read([os.path.expanduser(USER_CONFIG)]) - - if config.has_option(section, option): - if data_type == 'string': - to_return = config.get(section, option) - elif data_type == 'boolean': - to_return = config.getboolean(section, option) - return default if default else to_return - - return default if default else False - -def read_commandline(command): - '''Simple shell read access.''' - - return subprocess.check_output(command, shell=True) - -def execute(command): - '''Simple shell execute access.''' - - print 'Running command:\n\t%s\n' % command - subprocess.call(shlex.split(command)) - -def parse(): - '''Creates the argument parser for this tool.''' - - parser = argparse.ArgumentParser(description='command line tool for quick testing commands.') - parser.add_argument('--test', help='run a specific unit test') - parser.add_argument('--cassandra-version', help='run tests on a specific Cassandra version') - parser.add_argument('--upload', action='store_true', help='upload cobertura site to configured server') - parser.add_argument('--clean', action='store_true', help='runs the maven project from a clean environment') - parser.add_argument('--automated', action='store_true', help='ensures that runs do not get hung up by user input requests') - parser.add_argument('--testdocs', action='store_true', help='generates the test\'s Javadoc') - parser.add_argument('--samplecode', action='store_true', help='prints generated sample CQL') - args = parser.parse_args() - return args - -def check_path(): - '''Ensures this tool is run from the java-driver home directory.''' - - if not 'driver-core' in read_commandline('ls'): - sys.exit('Execute this command from your java-driver root directory.') - - -def maybe_setup_loopbacks(): - '''Currently only setting loopbacks for Mac OSX, but feel free to contribute for other setups.''' - - if platform.system() == 'Darwin' and not read_config('general', 'no_loopbacks', data_type='boolean'): - print 'Setting up CCM loopbacks...' - try: - loopbacks_enabled = read_commandline('ifconfig | grep "inet 127.0.1.4 netmask 0xff000000"') - if not loopbacks_enabled: - raise subprocess.CalledProcessError - except subprocess.CalledProcessError: - # For basic ccm - execute('sudo ifconfig lo0 alias 127.0.0.2 up') - execute('sudo ifconfig lo0 alias 127.0.0.3 up') - - # Additional loopbacks for the java-driver - execute('sudo ifconfig lo0 alias 127.0.1.1 up') - execute('sudo ifconfig lo0 alias 127.0.1.2 up') - execute('sudo ifconfig lo0 alias 127.0.1.3 up') - execute('sudo ifconfig lo0 alias 127.0.1.4 up') - execute('sudo ifconfig lo0 alias 127.0.1.5 up') - execute('sudo ifconfig lo0 alias 127.0.1.6 up') - -def maybe_upload_cobertura_site(): - ''' - The following must be set in the above defined USER_CONFIG: - [general] - cobertura_server = xxx - cobertura_directory = xxx - - If defined, the cobertura site will be rsync'd to a remote location. - ''' - - cobertura_server = read_config('general', 'cobertura_server') - cobertura_directory = read_config('general', 'cobertura_directory') - - if cobertura_server and cobertura_directory: - print 'rsync-ing cobertura site to %s:%s...' % ( - cobertura_server, - cobertura_directory) - execute('rsync -avz testing/cobertura-history %s:%s' % ( - cobertura_server, - cobertura_directory)) - -def save_cobertura_site(): - '''Save cobertura site folders by date''' - - execute('mkdir -p testing/cobertura-history') - today = datetime.date.today() - execute('cp -r driver-core/target/site/cobertura testing/cobertura-history/cobertura-%s' % today) - execute('rm -rf testing/cobertura-history/current') - execute('cp -r driver-core/target/site/cobertura testing/cobertura-history/current') - -def main(): - check_path() - args = parse() - - # Check if an upload is all that is required - if args.upload: - maybe_upload_cobertura_site() - sys.exit() - - if args.testdocs: - execute('mvn javadoc:test-javadoc') - print '\nTo view test Javadocs:' - print '\topen driver-core/target/site/testapidocs/index.html' - sys.exit() - - # Setup required ccm loopbacks - maybe_setup_loopbacks() - - # Start building the mvn command - cobertura_build_command = 'mvn' - - # Add the clean target, if asked or building the entire project - if args.clean or not args.test: - cobertura_build_command += ' clean' - - cobertura_build_command += ' versions:display-dependency-updates' # Ensures dependencies are up to date - cobertura_build_command += ' cobertura:cobertura' # Runs code coverage plugin - cobertura_build_command += ' --projects driver-core' # Runs only the main module - - if args.samplecode: - cobertura_build_command += ' -Pdoc' # Runs the docs "tests" and prints sample code - else: - cobertura_build_command += ' -Plong' # Runs the integration tests, not just tests - - # Use a specific Cassandra version, if asked - if args.cassandra_version: - cobertura_build_command += ' -Dcassandra.version=%s' % args.cassandra_version - - if args.test: - # Run against a single mvn test - execute('%s' - ' -Dmaven.test.failure.ignore=true' - ' -DfailIfNoTests=false' - ' -Dtest=%s' % (cobertura_build_command, args.test)) - else: - # Run against the entire integration suite - execute('%s' % cobertura_build_command) - - try: - if args.automated or raw_input('Save Cobertura Report? [y/N] ').lower() == 'y': - - # Save out cobertura site files - save_cobertura_site() - - # Perhaps move cobertura site files to a central location - maybe_upload_cobertura_site() - except KeyboardInterrupt: - print - - # Optionally, open coverage report when done building - if read_config('general', 'open_cobertura_site_after_build', 'boolean'): - execute('open driver-core/target/site/cobertura/index.html') - else: - print '\nTo view Cobertura report:' - print '\topen driver-core/target/site/cobertura/index.html' - - -if __name__ == '__main__': - main() diff --git a/upgrade_guide/README.md b/upgrade_guide/README.md index e85262ed847..ab684b23541 100644 --- a/upgrade_guide/README.md +++ b/upgrade_guide/README.md @@ -1,828 +1,431 @@ ## Upgrade guide -The purpose of this guide is to detail changes made by successive -versions of the Java driver. - -### 3.6.0 - -1. `ConsistencyLevel.LOCAL_SERIAL.isDCLocal()` now returns true. In driver - code, `isDCLocal()` is only used when evaluating a Statement's - ConsistencyLevel (which does not include Serial CLs), but as a matter of - correctness this was updated. - -2. `ReadFailureException` and `WriteFailureException` are now surfaced to - `RetryPolicy.onRequestError`. Consider updating custom `RetryPolicy` - implementations to account for this. In the general case, we recommend - using `RetryDecision.rethrow()`, see [JAVA-1944]. - -[JAVA-1944]: https://datastax-oss.atlassian.net/browse/JAVA-1944 - - -### 3.5.0 - -1. The `DowngradingConsistencyRetryPolicy` is now deprecated, see [JAVA-1752]. - It will also be removed in the next major release of the driver (4.0.0), - see [JAVA-1376]. - - The main motivation is the agreement that this policy's behavior should be - the application's concern, not the driver's. - - We recognize that there are use cases where downgrading is good – - for instance, a dashboard application would present the latest information - by reading at QUORUM, but it's acceptable for it to display stale information - by reading at ONE sometimes. - - But APIs provided by the driver should instead encourage idiomatic use of - a distributed system like Apache Cassandra, and a downgrading policy works - against this. It suggests that an anti-pattern such as "try to read at QUORUM, - but fall back to ONE if that fails" is a good idea in general use cases, - when in reality it provides no better consistency guarantees than working - directly at ONE, but with higher latencies. - - We therefore urge users to carefully choose upfront the consistency level that - works best for their use cases, and should they decide that the downgrading - behavior of `DowngradingConsistencyRetryPolicy` remains a good fit for certain - use cases, they will now have to implement this logic themselves, either - at application level, or alternatively at driver level, by rolling out their - own downgrading retry policy. - - To help users migrate existing applications that rely on - `DowngradingConsistencyRetryPolicy`, see this [online example] that illustrates - how to implement a downgrading logic at application level. - -[JAVA-1752]:https://datastax-oss.atlassian.net/browse/JAVA-1752 -[JAVA-1376]:https://datastax-oss.atlassian.net/browse/JAVA-1376 -[online example]:https://github.com/datastax/java-driver/blob/3.x/driver-examples/src/main/java/com/datastax/driver/examples/retry/DowngradingRetry.java - -2. The `TokenAwarePolicy` now has a new constructor that takes a `ReplicaOrdering` - argument, see [JAVA-1448]. - - One of the advantages of this feature is the new `NEUTRAL` - ordering strategy, which honors its child policy's ordering, i.e., replicas - are returned in the same relative order as in the child policy's query plan. - - For example, if the child policy returns the plan [A, B, C, D], and the replicas - for the query being routed are [D, A, B], then the token aware policy would return - the plan [A, B, D, C]. +### 4.0.0 - As a consequence, the constructor taking a boolean parameter `shuffleReplicas` - is now deprecated and will be removed in the next major release. - -[JAVA-1448]:https://datastax-oss.atlassian.net/browse/JAVA-1448 +Version 4 is major redesign of the internal architecture. As such, it is **not binary compatible** +with previous versions. However, most of the concepts remain unchanged, and the new API will look +very familiar to 2.x and 3.x users. +#### New Maven coordinates -### 3.4.0 - -`QueryBuilder` methods `in`, `lt`, `lte`, `eq`, `gt`, and `gte` now accept -`Iterable` as input rather than just `List`. This should have no impact unless -you were accessing these methods using reflection in which case you need to -account for these new parameter types. - - -### 3.3.1 - -Speculative executions can now be scheduled without delay: if -`SpeculativeExecutionPlan.nextExecution()` returns 0, the next execution will be fired immediately. -This allows aggressive policies that hit multiple replicas right away, in order to get the fastest -response possible. Note that this may break existing policies that used 0 to mean "no execution"; -make sure you use a negative value instead. - - -### 3.2.0 - -The `SSLOptions` interface is now deprecated in favor of -`RemoteEndpointAwareSSLOptions`. -Similarly, the two existing implementations of that interface, -`JdkSSLOptions` and `NettySSLOptions`, -are now deprecated in favor of `RemoteEndpointAwareJdkSSLOptions` -and `RemoteEndpointAwareNettySSLOptions` respectively (see -[JAVA-1364](https://datastax-oss.atlassian.net/browse/JAVA-1364)). - -In 3.1.0, the driver would log a warning the first time it would skip -a retry for a non-idempotent request; this warning has now been -removed as users should now have adjusted their applications accordingly. - -The `caseSensitive` field on `@Column` and `@Field` annotation now only -applies to the `name` field on the annotation and not the name of the -variable / method itself. If you were previously depending on the -name of the field, you should add a `name` field to the annotation, -i.e.: `@Column(name="userName", caseSensitive=true)`. - - -### 3.1.0 - -This version introduces an important change in the default retry behavior: statements that are not idempotent are not -always retried automatically anymore. - -Prior to 2.1.10, idempotence was not considered for retries. This exposed applications to the risk of applying a -non-idempotent statement twice (counter increment, list append...), or to more subtle bugs with lightweight transactions -(see [JAVA-819](https://datastax-oss.atlassian.net/browse/JAVA-819)). - -In 2.1.10 / 3.0.x, we introduced `IdempotenceAwareRetryPolicy`, which considers the `Statement#isIdempotent()` in the -retry decision process. However, for consistency with previous versions, this policy was not enabled by default (in -particular because statements are non-idempotent by default, and we didn't want applications to suddenly stop retrying -queries that were retried before). - -In 3.1.0, the default is now to **not retry** after a write timeout or request error if the statement is not idempotent. -This is handled internally, the retry policy methods are not even invoked in those cases (and therefore -`IdempotenceAwareRetryPolicy` has been deprecated). See the manual section about [retries](../manual/retries/) for more -information. - -In practice, here's what upgrading to 3.1.0 means for you: - -* if you were already handling idempotence in your application, there won't be any change, but you can stop wrapping - your retry policy with `IdempotenceAwareRetryPolicy`; -* otherwise, you might want to review how your code positions the `setIdempotent` flag on statements. In most cases the - driver can't compute in automatically (because it doesn't parse query strings), so it takes a conservative approach - and sets it to `false` by default. If you know the query is idempotent, you should set it to `true` manually. See the - [query idempotence](../manual/idempotence/) section of the manual. - -The driver logs a warning the first time it ignores a non-idempotent request; this warning will be removed in version -3.2.0. - - -### 3.0.4 - -The connection pool is now fully non-blocking ([JAVA-893](https://datastax-oss.atlassian.net/browse/JAVA-893)), -which greatly improves asynchronous programming: - -* `Session.executeAsync` won't ever block the user thread anymore; -* you can now safely run async queries on a session connected to a keyspace. - -This new implementation brings a couple of changes: - -* pool saturation is no longer handled by a timeout, but instead by a bounded queue. As a consequence, - `PoolingOptions.setPoolTimeoutMillis` has been deprecated, and replaced by `setMaxQueueSize`; -* a saturated pool will now throw `BusyPoolException` instead of `TimeoutException` (note that this exception is not - rethrown directly to the client, but wrapped in `NoHostAvailableException.getErrors()`). - - -### 3.0.0 - -This version brings parity with Cassandra 2.2 and 3.0. - -It is **not binary compatible** with the driver's 2.1 branch. -The main changes were introduced by the custom codecs feature (see below). -We've also seized the opportunity to remove code that was deprecated in 2.1. - -1. The default consistency level in `QueryOptions` is now `LOCAL_ONE`. -2. [Custom codecs](../manual/custom_codecs/) - ([JAVA-721](https://datastax-oss.atlassian.net/browse/JAVA-721)) - introduce several breaking changes and also modify a few runtime behaviors. - - Here is a detailed list of breaking API changes: - * `TypeCodec` was package-private before and is now public. - * `DataType` has no more references to `TypeCodec`, so methods that dealt with serialization and deserialization of - data types have been removed: - * `ByteBuffer serialize(Object value, ProtocolVersion protocolVersion)` - * `ByteBuffer serializeValue(Object value, ProtocolVersion protocolVersion)` - * `Object deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion)` - * `Object deserialize(ByteBuffer bytes, int protocolVersion)` - * `Object parse(String value)` - * `String format(Object value)` - * `Class asJavaClass()` - - These methods must now be invoked on `TypeCodec` directly. To resolve the `TypeCodec` instance for a particular - data type, use `CodecRegistry#codecFor`. - * `GettableByIndexData` (affects `Row`, `BoundStatement`, `TupleValue` and `UDTValue`). The following public methods were added: - * ` T get(int i, Class targetClass)` - * ` T get(int i, TypeToken targetType)` - * ` T get(int i, TypeCodec codec)` - * `GettableByNameData` (affects `Row`, `BoundStatement` and `UDTValue`). The following public methods were added: - * ` T get(String name, Class targetClass)` - * ` T get(String name, TypeToken targetType)` - * ` T get(String name, TypeCodec codec)` - * `SettableByIndexData` (affects `Row`, `BoundStatement`, `TupleValue` and `UDTValue`). The following public methods were added: - * ` T set(int i, V v, Class targetClass)` - * ` T set(int i, V v, TypeToken targetType)` - * ` T set(int i, V v, TypeCodec codec)` - * `SettableByNameData` (affects `Row`, `BoundStatement` and `UDTValue`). The following public methods were added: - * ` T set(String name, V v, Class targetClass)` - * ` T set(String name, V v, TypeToken targetType)` - * ` T set(String name, V v, TypeCodec codec)` - * `Statement`. The following public methods were modified: - * `getRoutingKey(ProtocolVersion, CodecRegistry)`: both parameters added. - * `RegularStatement`. The following public methods were modified: - * `getValues(ProtocolVersion, CodecRegistry)`: second parameter added. - * `getQueryString(CodecRegistry)` and `hasValues(CodecRegistry)`: parameter added. No-arg versions are still present - and use the default codec registry; refer to the Javadocs for guidance on which version to use. - * `PreparedStatement`. The following public method was added: - * `CodecRegistry getCodecRegistry()`. - * `TupleType`. The following public method was deleted: - * `TupleType of(DataType... types)`; users should now use `Metadata.newTupleType(DataType...)`. - -

The driver runtime behavior changes in the following situations:

- * By default, the driver now returns _mutable_, _non thread-safe_ instances for CQL collection types; - This affects methods `getList`, `getSet`, `getMap`, `getObject` and `get` for all instances of - `GettableByIndexData` and `GettableByNameData` (`Row`, `BoundStatement`, `TupleValue` and `UDTValue`) - * `RuntimeException`s thrown during serialization or deserialization might not be - the same ones as before, due to the newly-introduced `CodecNotFoundException` - and to the dynamic nature of codec search introduced by JAVA-721. - * `TypeCodec.format(Object)` now returns the CQL keyword `"NULL"` instead of a `null` reference - for `null` inputs. - -3. The driver now depends on Guava 16.0.1 (instead of 14.0.1). - This update has been mainly motivated by Guava's [Issue #1635](https://github.com/google/guava/issues/1635), - which affects `TypeToken`, and hence all `TypeCodec` implementations handling parameterized types. - -4. `UDTMapper` (the type previously used to convert `@UDT`-annotated - classes to their CQL counterpart) was removed, as well as the - corresponding method `MappingManager#udtMapper`. - - The mapper now uses custom codecs to convert UDTs. See more - explanations [here](../manual/object_mapper/custom_codecs/#implicit-udt-codecs). - -5. All methods that took the protocol version as an `int` or assumed a - default version have been removed (they were already deprecated in - 2.1): - * `AbstractGettableData(int)` - * `Cluster.Builder#withProtocolVersion(int)` - * in `ProtocolOptions`: - * `NEWEST_SUPPORTED_PROTOCOL_VERSION` (replaced by - `ProtocolVersion#NEWEST_SUPPORTED`) - * `int getProtocolVersion()` - - There are now variants of these methods using the `ProtocolVersion` - enum. In addition, `ProtocolOptions#getProtocolVersionEnum` has been - renamed to `ProtocolOptions#getProtocolVersion`. - -6. All methods related to the "suspected" host state have been removed - (they had been deprecated in 2.1.6 when the suspicion mechanism was - removed): - * `Host.StateListener#onSuspected()` (was inherited by - `LoadBalancingPolicy`) - * `Host#getInitialReconnectionAttemptFuture()` - -7. `PoolingOptions#setMinSimultaneousRequestsPerConnectionThreshold(HostDistance, - int)` has been removed. The new connection pool resizing algorithm introduced by - [JAVA-419](https://datastax-oss.atlassian.net/browse/JAVA-419) does not need this - threshold anymore. - -8. `AddressTranslater` has been renamed to `AddressTranslator`. All - related methods and classes have also been renamed. - - In addition, the `close()` method has been pulled up into - `AddressTranslator`, and `CloseableAddressTranslator` has been removed. - Existing third-party `AddressTranslator` implementations only need - to add an empty `close()` method. - -9. The `close()` method has been pulled up into `LoadBalancingPolicy`, - and `CloseableLoadBalancingPolicy` has been removed. Existing third-party - `LoadBalancingPolicy` implementations only need to add an empty - `close()` method. - -10. All pluggable components now have callbacks to detect when they get - associated with a `Cluster` instance: - * `ReconnectionPolicy`, `RetryPolicy`, `AddressTranslator`, - and `TimestampGenerator`: - * `init(Cluster)` - * `close()` - * `Host.StateListener` and `LatencyTracker`: - * `onRegister(Cluster)` - * `onUnregister(Cluster)` - - This gives these components the opportunity to perform - initialization / cleanup tasks. Existing third-party implementations - only need to add empty methods. - -11. `LoadBalancingPolicy` does not extend `Host.StateListener` anymore: - callback methods (`onUp`, `onDown`, etc.) have been duplicated. This - is unlikely to affect clients. - -12. [Client-side timestamp generation](../manual/query_timestamps/) is - now the default (provided that [native - protocol](../manual/native_protocol) v3 or higher is in use). The - generator used is `AtomicMonotonicTimestampGenerator`. - -13. If a DNS name resolves to multiple A-records, - `Cluster.Builder#addContactPoint(String)` will now use all of these - addresses as contact points. This gives you the possibility of - maintaining contact points in DNS configuration, and having a single, - static contact point in your Java code. - -14. The following methods were added for [Custom payloads](../manual/custom_payloads): - * in `PreparedStatement`: `getIncomingPayload()`, - `getOutgoingPayload()` and - `setOutgoingPayload(Map)` - * `AbstractSession#prepareAsync(String, Map)` - - Also, note that `AbstractSession#prepareAsync(Statement)` does not - call `AbstractSession#prepareAsync(String)` anymore, they now both - delegate to a protected method. - - This breaks binary compatibility for these two classes; if you have - custom implementations, you will have to adapt them accordingly. - -15. Getters and setters have been added to "data-container" classes for - new CQL types: - * `getByte`/`setByte` for the `TINYINT` type - * `getShort`/`setShort` for the `SMALLINT` type - * `getTime`/`setTime` for the `TIME` type - * `getDate`/`setDate` for the `DATE` type - - The methods for the `TIMESTAMP` CQL type have been renamed to - `getTimestamp` and `setTimestamp`. - - This affects `Row`, `BoundStatement`, `TupleValue` and `UDTValue`. - -16. New exception types have been added to handle additional server-side - errors introduced in Cassandra 2.2: - * `ReadFailureException` - * `WriteFailureException` - * `FunctionExecutionException` - - This is not a breaking change since all driver exceptions are - unchecked; but clients might decide to handle these errors in a specific - way. - - In addition, `QueryTimeoutException` has been renamed to - `QueryExecutionException` (this is an intermediary class in our - exception hierarchy, it now has new child classes that are not - related to timeouts). - -17. `ResultSet#fetchMoreResults()` now returns a `ListenableFuture`. - This makes the API more friendly if you chain transformations on an async - query to process all pages (see `AsyncResultSetTest` in the sources for an - example). - -18. `Frozen` annotations in the mapper are no longer checked at runtime (see - [JAVA-843](https://datastax-oss.atlassian.net/browse/JAVA-843) for more - explanations). So they become purely informational at this stage. - However it is a good idea to keep using these annotations and make sure - they match the schema, in anticipation for the schema generation features - that will be added in a future version. - -19. `AsyncInitSession` has been removed, `initAsync()` is now part of the - `Session` interface (the only purpose of the extra interface was to preserve - binary compatibility on the 2.1 branch). - -20. `TableMetadata.Options` has been made a top-level class and renamed to - `TableOptionsMetadata`. It is now also used by `MaterializedViewMetadata`. - -21. The mapper annotation `@Enumerated` has been removed, users should now - use the newly-introduced `driver-extras` module to get automatic - enum-to-CQL mappings. Two new codecs provide the same functionality: - `EnumOrdinalCodec` and `EnumNameCodec`: - - ```java - enum Foo {...} - enum Bar {...} - - // register the appropriate codecs - CodecRegistry.DEFAULT_INSTANCE - .register(new EnumOrdinalCodec(Foo.class)) - .register(new EnumNameCodec(Bar.class)) - - // the following mappings are handled out-of-the-box - @Table - public class MyPojo { - private Foo foo; - private List bars; - ... - } - ``` - -22. The interface `IdempotenceAwarePreparedStatement` has been removed - and now the `PreparedStatement` interface exposes 2 new methods, - `setIdempotent(Boolean)` and `isIdempotent()`. - -23. `RetryPolicy` and `ExtendedRetryPolicy` (introduced in 2.1.10) - were merged together; as a consequence, `RetryPolicy` now has one - more method: `onRequestError`; see - [JAVA-819](https://datastax-oss.atlassian.net/browse/JAVA-819) for - more information. Furthermore, `FallthroughRetryPolicy` now returns - `RetryDecision.rethrow()` when `onRequestError` is called. - -24. `DseAuthProvider` has been deprecated and is now replaced by - `DseGSSAPIAuthProvider` for Kerberos authentication. `DsePlainTextAuthProvider` - has been introduced to handle plain text authentication with the - `DseAuthenticator`. - -25. The constructor of `DCAwareRoundRobinPolicy` is not accessible anymore. You - should use `DCAwareRoundRobinPolicy#builder()` to create new instances. - -26. `ColumnMetadata.getTable()` has been renamed to `ColumnMetadata.getParent()`. - Also, its return type is now `AbstractTableMetadata` which can be either - a `TableMetadata` object or a `MaterializedViewMetadata` object. - This change is motivated by the fact that a column can now belong to a - table or a materialized view. - -27. `ColumnMetadata.getIndex()` has been removed. - This is due to the fact that secondary indexes have been completely redesigned - in Cassandra 3.0, and the former one-to-one relationship between a column and its index - has been replaced with a one-to-many relationship between a table and its indexes. - This is reflected in the driver's API by the new methods - `TableMetadata.getIndexes()` and `TableMetadata.getIndex(String name)`. - See [CASSANDRA-9459](https://issues.apache.org/jira/browse/CASSANDRA-9459) and - and [JAVA-1008](https://datastax-oss.atlassian.net/browse/JAVA-1008) for - more details. - Unfortunately, there is no easy way to recover the functionality provided - by the deleted method, _even for Cassandra versions <= 3.0_. - -28. `IndexMetadata` is now a top-level class and its structure has been deeply modified. - Again, this is due to the fact that secondary indexes have been completely redesigned - in Cassandra 3.0. - -29. `SSLOptions` has been refactored to allow the option to choose between JDK and Netty-based - SSL implementations. See [JAVA-841](https://datastax-oss.atlassian.net/browse/JAVA-841) and - the [SSL documentation](../manual/ssl) for more details. - - -### 2.1.8 - -2.1.8 is binary-compatible with 2.1.7 but introduces a small change in the -driver's behavior: - -1. The list of contact points provided at startup is now shuffled before trying - to open the control connection, so that multiple clients with the same contact - points don't all pick the same control host. As a result, you can't assume that - the driver will try contact points in a deterministic order. In particular, if - you use the `DCAwareRoundRobinPolicy` without specifying a primary datacenter - name, make sure that you only provide local hosts as contact points. - - -### 2.1.7 - -This version brings a few changes in the driver's behavior; none of them break -binary compatibility. - -1. The `DefaultRetryPolicy`'s behaviour has changed in the case of an Unavailable - exception received from a request. The new behaviour will cause the driver to - process a Retry on a different node at most once, otherwise an exception will - be thrown. This change makes sense in the case where the node tried initially - for the request happens to be isolated from the rest of the cluster (e.g. - because of a network partition) but can still answer to the client normally. - In this case, trying another node has a chance of success. - The previous behaviour was to always throw an exception. +The core driver is available from: -2. The following properties in `PoolingOptions` were renamed: - * `MaxSimultaneousRequestsPerConnectionThreshold` to `NewConnectionThreshold` - * `MaxSimultaneousRequestsPerHostThreshold` to `MaxRequestsPerConnection` - - The old getters/setters were deprecated, but they delegate to the new - ones. - - Also, note that the connection pool for protocol v3 can now be configured to - use multiple connections. See [this page](../manual/pooling) for more - information. +```xml + + com.datastax.oss + java-driver-core + 4.0.0 + +``` -3. `MappingManager(Session)` will now force the initialization of the `Session` - if needed. This is a change from 2.1.6, where if you gave it an uninitialized - session (created with `Cluster#newSession()` instead of `Cluster#connect()`), - it would only get initialized on the first request. +#### Runtime requirements - If this is a problem for you, `MappingManager(Session, ProtocolVersion)` - preserves the previous behavior (see the API docs for more details). - -4. A `BuiltStatement` is now considered non-idempotent whenever a `fcall()` - or `raw()` is used to build a value to be inserted in the database. - If you know that the CQL functions or expressions are safe, use - `setIdempotent(true)` on the statement. +The driver now requires **Java 8 or above**. It does not depend on Guava anymore (we still use it +internally but it's shaded). -### 2.1.6 +We have dropped support for legacy protocol versions v1 and v2. As a result, the driver is +compatible with: -See [2.0.10](#2-0-x-to-2-0-10). +* **Apache Cassandra®: 2.1 and above**; +* **Datastax Enterprise: 4.7 and above**. +#### Packages -### 2.1.2 +We've adopted new [API conventions] to better organize the driver code and make it more modular. As +a result, package names have changed. However most public API types have the same names; you can use +the auto-import or "find class" features of your IDE to discover the new locations. -2.1.2 brings important internal changes with native protocol v3 support, but -the impact on the public API has been kept as low as possible. +Here's a side-by-side comparison with the legacy driver for a basic example: -#### User API Changes +```java +// Driver 3: +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.SimpleStatement; -1. The native protocol version is now modelled as an enum: `ProtocolVersion`. - Most public methods that take it as an argument have a backward-compatible - version that takes an `int` (the exception being `RegularStatement`, - described below). For new code, prefer the enum version. +SimpleStatement statement = + new SimpleStatement("SELECT release_version FROM system.local"); +ResultSet resultSet = session.execute(statement); +Row row = resultSet.one(); +System.out.println(row.getString("release_version")); -#### Internal API Changes -1. `RegularStatement.getValues` now takes the protocol version as a - `ProtocolVersion` instead of an `int`. This is transparent for callers - since there is a backward-compatible alternative, but if you happened to - extend the class you'll need to update your implementation. +// Driver 4: +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.cql.SimpleStatement; -2. `BatchStatement.setSerialConsistencyLevel` now returns `BatchStatement` - instead of `Statement`. Again, this only matters if you extended this - class (if so, it might be a good idea to also have a covariant return in - your child class). +SimpleStatement statement = + SimpleStatement.newInstance("SELECT release_version FROM system.local"); +ResultSet resultSet = session.execute(statement); +Row row = resultSet.one(); +System.out.println(row.getString("release_version")); +``` -3. The constructor of `UnsupportedFeatureException` now takes a - `ProtocolVersion` as a parameter. This should impact few users, as there's - hardly any reason to build instances of that class from client code. +Notable changes: -#### New features +* the imports; +* simple statement instances are now created with the `newInstance` static factory method. This is + because `SimpleStatement` is now an interface (as most public API types). -These features are only active when the native protocol v3 is in use. +[API conventions]: ../manual/api_conventions -1. The driver now uses a single connection per host (as opposed to a pool in - 2.1.1). Most options in `PoolingOptions` are ignored, except for a new one - called `maxSimultaneousRequestsPerHostThreshold`. See the class's Javadocs - for detailed explanations. +#### Configuration -2. You can now provide a default timestamp with each query (but it will be - ignored if the CQL query string already contains a `USING TIMESTAMP` - clause). This can be done on a per-statement basis with - `Statement.setDefaultTimestamp`, or automatically with a - `TimestampGenerator` specified with - `Cluster.Builder.withTimestampGenerator` (two implementations are - provided: `ThreadLocalMonotonicTimestampGenerator` and - `AtomicMonotonicTimestampGenerator`). If you specify both, the statement's - timestamp takes precedence over the generator. By default, the driver has - the same behavior as 2.1.1 (no generator, timestamps are assigned by - Cassandra unless `USING TIMESTAMP` was specified). +The configuration has been completely revamped. Instead of ad-hoc configuration classes, the default +mechanism is now file-based, using the [Typesafe Config] library. This is a better choice for most +deployments, since it allows configuration changes without recompiling the client application (note +that there are still programmatic setters for things that are likely to be injected dynamically, +such as contact points). + +The driver JAR contains a `reference.conf` file that defines the options with their defaults: + +``` +datastax-java-driver { + basic.request { + timeout = 2 seconds + consistency = LOCAL_ONE + page-size = 5000 + } + // ... and many more (~10 basic options, 70 advanced ones) +} +``` -3. `BatchStatement.setSerialConsistencyLevel` no longer throws an exception, - it will honor the serial consistency level for the batch. +You can place an `application.conf` in your application's classpath to override options selectively: +``` +datastax-java-driver { + basic.request.consistency = ONE +} +``` -### 2.1.1 +Options can also be overridden with system properties when launching your application: -#### Internal API Changes +``` +java -Ddatastax-java-driver.basic.request.consistency=ONE MyApp +``` -1. The `ResultSet` interface has a new `wasApplied()` method. This will - only affect clients that provide their own implementation of this interface. +The configuration also supports *execution profiles*, that allow you to capture and reuse common +sets of options: +```java +// application.conf: +datastax-java-driver { + profiles { + profile1 { basic.request.consistency = QUORUM } + profile2 { basic.request.consistency = ONE } + } +} -### 2.1.0 +// Application code: +SimpleStatement statement1 = + SimpleStatement.newInstance("...").setExecutionProfileName("profile1"); +SimpleStatement statement2 = + SimpleStatement.newInstance("...").setExecutionProfileName("profile2"); +``` -#### User API Changes +The configuration can be reloaded periodically at runtime: -1. The `getCaching` method of `TableMetadata#Options` now returns a - `Map` to account for changes to Cassandra 2.1. Also, the - `getIndexInterval` method now returns an `Integer` instead of an `int` - which will be `null` when connected to Cassandra 2.1 nodes. +``` +datastax-java-driver { + basic.config-reload-interval = 5 minutes +} +``` + +This is fully customizable: the configuration is exposed to the rest of the driver as an abstract +`DriverConfig` interface; if the default implementation doesn't work for you, you can write your +own. + +For more details, refer to the [manual](../manual/core/configuration). + +[Typesafe Config]: https://github.com/typesafehub/config + +#### Session + +`Cluster` does not exist anymore; the session is now the main component, initialized in a single +step: + +```java +CqlSession session = CqlSession.builder().build(); +session.execute("..."); +``` + +Protocol negotiation in mixed clusters has been improved: you no longer need to force the protocol +version during a rolling upgrade. The driver will detect that there are older nodes, and downgrade +to the best common denominator (see +[JAVA-1295](https://datastax-oss.atlassian.net/browse/JAVA-1295)). + +Reconnection is now possible at startup: if no contact point is reachable, the driver will retry at +periodic intervals (controlled by the [reconnection policy](../manual/core/reconnection/)) instead +of throwing an error. To turn this on, set the following configuration option: + +``` +datastax-java-driver { + advanced.reconnect-on-init = true +} +``` + +The session now has a built-in [throttler](../manual/core/throttling/) to limit how many requests +can execute concurrently. Here's an example based on the number of requests (a rate-based variant is +also available): + +``` +datastax-java-driver { + advanced.throttler { + class = ConcurrencyLimitingRequestThrottler + max-concurrent-requests = 10000 + max-queue-size = 100000 + } +} +``` + +#### Load balancing policy + +Previous driver versions came with multiple load balancing policies that could be nested into each +other. In our experience, this was one of the most complicated aspects of the configuration. + +In driver 4, we are taking a more opinionated approach: we provide a single [default +policy](../manual/core/load_balancing/#default-policy), with what we consider as the best practices: + +* local only: we believe that failover should be handled at infrastructure level, not by application + code. +* token-aware. +* optionally filtering nodes with a custom predicate. + +You can still provide your own policy by implementing the `LoadBalancingPolicy` interface. + +#### Statements + +Simple, bound and batch [statements](../manual/core/statements/) are now exposed in the public API +as interfaces. The internal implementations are immutable. This makes them automatically +thread-safe: you don't need to worry anymore about sharing them or reusing them between asynchronous +executions. + +Note that all mutating methods return a new instance, so make sure you don't accidentally ignore +their result: + +```java +BoundStatement boundSelect = preparedSelect.bind(); + +// This doesn't work: setInt doesn't modify boundSelect in place: +boundSelect.setInt("k", key); +session.execute(boundSelect); + +// Instead, do this: +boundSelect = boundSelect.setInt("k", key); +``` + +These methods are annotated with `@CheckReturnValue`. Some code analysis tools -- such as +[ErrorProne](https://errorprone.info/) -- can check correct usage at build time, and report mistakes +as compiler errors. + +Unlike 3.x, the request timeout now spans the entire request. In other words, it's the +maximum amount of time that `session.execute` will take, including any retry, speculative execution, +etc. You can set it with `Statement.setTimeout`, or globally in the configuration with the +`basic.request.timeout` option. + +[Prepared statements](../manual/core/statements/prepared/) are now cached client-side: if you call +`session.prepare()` twice with the same query string, it will no longer log a warning. The second +call will return the same statement instance, without sending anything to the server: + +```java +PreparedStatement ps1 = session.prepare("SELECT * FROM product WHERE sku = ?"); +PreparedStatement ps2 = session.prepare("SELECT * FROM product WHERE sku = ?"); +assert ps1 == ps2; +``` + +This cache takes into account all execution parameters. For example, if you prepare the same query +string with different consistency levels, you will get two distinct prepared statements, each +propagating its own consistency level to its bound statements: + +```java +PreparedStatement ps1 = + session.prepare( + SimpleStatement.newInstance("SELECT * FROM product WHERE sku = ?") + .setConsistencyLevel(DefaultConsistencyLevel.ONE)); +PreparedStatement ps2 = + session.prepare( + SimpleStatement.newInstance("SELECT * FROM product WHERE sku = ?") + .setConsistencyLevel(DefaultConsistencyLevel.TWO)); + +assert ps1 != ps2; + +BoundStatement bs1 = ps1.bind(); +assert bs1.getConsistencyLevel() == DefaultConsistencyLevel.ONE; + +BoundStatement bs2 = ps2.bind(); +assert bs2.getConsistencyLevel() == DefaultConsistencyLevel.TWO; +``` + +#### Dual result set APIs + +In 3.x, both synchronous and asynchronous execution models shared a common result set +implementation. This made asynchronous usage [notably error-prone][3.x async paging], because of the +risk of accidentally triggering background synchronous fetches. + +There are now two separate APIs: synchronous queries return a `ResultSet`; asynchronous queries +return a future of `AsyncResultSet`. + +`ResultSet` behaves much like its 3.x counterpart, except that background pre-fetching +(`fetchMoreResults`) was deliberately removed, in order to keep this interface simple and intuitive. +If you were using synchronous iterations with background pre-fetching, you should now switch to +fully asynchronous iterations (see below). + +`AsyncResultSet` is a simplified type that only contains the rows of the current page. When +iterating asynchronously, you no longer need to stop the iteration manually: just consume all the +rows in `currentPage()`, and then call `fetchNextPage` to retrieve the next page asynchronously. You +will find more information about asynchronous iterations in the manual pages about [asynchronous +programming][4.x async programming] and [paging][4.x paging]. + +[3.x async paging]: http://docs.datastax.com/en/developer/java-driver/3.2/manual/async/#async-paging +[4.x async programming]: ../manual/core/async/ +[4.x paging]: ../manual/core/paging/ + +#### CQL to Java type mappings + +Since the driver now has access to Java 8 types, some of the [CQL to Java type mappings] have +changed when it comes to [temporal types] such as `date` and `timestamp`: + +* `getDate` has been replaced by `getLocalDate` and returns [java.time.LocalDate]; +* `getTime` has been replaced by `getLocalTime` and returns [java.time.LocalTime] instead of a + `long` representing nanoseconds since midnight; +* `getTimestamp` has been replaced by `getInstant` and returns [java.time.Instant] instead of + [java.util.Date]. + +The corresponding setter methods were also changed to expect these new types as inputs. + +[CQL to Java type mappings]: ../manual/core#cql-to-java-type-mapping +[temporal types]: ../manual/core/temporal_types +[java.time.LocalDate]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html +[java.time.LocalTime]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalTime.html +[java.time.Instant]: https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html +[java.util.Date]: https://docs.oracle.com/javase/8/docs/api/java/util/Date.html + +#### Metrics + +[Metrics](../manual/core/metrics/) are now divided into two categories: session-wide and per-node. +Each metric can be enabled or disabled individually in the configuration: + +``` +datastax-java-driver { + advanced.metrics { + // more are available, see reference.conf for the full list + session.enabled = [ bytes-sent, bytes-received, cql-requests ] + node.enabled = [ bytes-sent, bytes-received, pool.in-flight ] + } +} +``` + +Note that unlike 3.x, JMX is not supported out of the box. You'll need to add the dependency +explicitly: + +```xml + + io.dropwizard.metrics + metrics-jmx + 4.0.2 + +``` + +#### Metadata + +`Session.getMetadata()` is now immutable and updated atomically. The node list, schema metadata and +token map exposed by a given `Metadata` instance are guaranteed to be in sync. This is convenient +for analytics clients that need a consistent view of the cluster at a given point in time; for +example, a keyspace in `metadata.getKeyspaces()` will always have a corresponding entry in +`metadata.getTokenMap()`. + +On the other hand, this means you have to call `getMetadata()` again each time you need a fresh +copy; do not cache the result: + +```java +Metadata metadata = session.getMetadata(); +Optional ks = metadata.getKeyspace("test"); +assert !ks.isPresent(); + +session.execute( + "CREATE KEYSPACE IF NOT EXISTS test " + + "WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); -2. `BoundStatement` variables that have not been set explicitly will no - longer default to `null`. Instead, all variables must be bound explicitly, - otherwise the execution of the statement will fail (this also applies to - statements inside of a `BatchStatement`). For variables that map to a - primitive Java type, a new `setToNull` method has been added. - We made this change because the driver might soon distinguish between unset - and null variables, so we don't want clients relying on the "leave unset to - set to `null`" behavior. +// This is still the same metadata from before the CREATE +ks = metadata.getKeyspace("test"); +assert !ks.isPresent(); + +// You need to fetch the whole metadata again +metadata = session.getMetadata(); +ks = metadata.getKeyspace("test"); +assert ks.isPresent(); +``` + +Refreshing the metadata can be CPU-intensive, in particular the token map. To help alleviate that, +it can now be filtered to a subset of keyspaces. This is useful if your application connects to a +shared cluster, but does not use the whole schema: + +``` +datastax-java-driver { + // defaults to empty (= all keyspaces) + advanced.metadata.schema.refreshed-keyspaces = [ "users", "products" ] +} +``` + +See the [manual](../manual/core/metadata/) for all the details. + +#### Query builder + +The query builder is now distributed as a separate artifact: + +```xml + + com.datastax.oss + java-driver-query-builder + 4.0.0 + +``` + +It is more cleanly separated from the core driver, and only focuses on query string generation. +Built queries are no longer directly executable, you need to convert them into a string or a +statement: + +```java +import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.*; + +BuildableQuery query = + insertInto("user") + .value("id", bindMarker()) + .value("first_name", bindMarker()) + .value("last_name", bindMarker()); + +String cql = query.asCql(); +// INSERT INTO user (id,first_name,last_name) VALUES (?,?,?) + +SimpleStatement statement = query + .builder() + .addNamedValue("id", 0) + .addNamedValue("first_name", "Jane") + .addNamedValue("last_name", "Doe") + .build(); +``` + +All query builder types are immutable, making them inherently thread-safe and share-safe. + +The query builder has its own [manual chapter](../manual/query_builder/), where the syntax is +covered in detail. + +#### Dedicated type for CQL identifiers + +Instead of raw strings, the names of schema objects (keyspaces, tables, columns, etc.) are now +wrapped in a dedicated `CqlIdentifier` type. This avoids ambiguities with regard to [case +sensitivity](../manual/case_sensitivity). + +#### Pluggable request execution logic +`Session` is now a high-level abstraction capable of executing arbitrary requests. Out of the box, +the driver exposes a more familiar subtype `CqlSession`, that provides familiar signatures for CQL +queries (`execute(Statement)`, `prepare(String)`, etc). -#### Internal API Changes +However, the request execution logic is completely pluggable, and supports arbitrary request types +(as long as you write the boilerplate to convert them to protocol messages). -The changes listed in this section should normally not impact end users of the -driver, but rather third-party frameworks and tools. - -1. The `serialize` and `deserialize` methods in `DataType` now take an - additional parameter: the protocol version. As explained in the javadoc, - if unsure, the proper value to use for this parameter is the protocol version - in use by the driver, i.e. the value returned by - `cluster.getConfiguration().getProtocolOptions().getProtocolVersion()`. - -2. The `parse` method in `DataType` now returns a Java object, not a - `ByteBuffer`. The previous behavior can be obtained by calling the - `serialize` method on the returned object. - -3. The `getValues` method of `RegularStatement` now takes the protocol - version as a parameter. As above, the proper value if unsure is almost surely - the protocol version in use - (`cluster.getConfiguration().getProtocolOptions().getProtocolVersion()`). - - -### 2.0.11 - -2.0.11 preserves binary compatibility with previous versions. There are a few -changes in the driver's behavior: - -1. The `DefaultRetryPolicy`'s behaviour has changed in the case of an Unavailable - exception received from a request. The new behaviour will cause the driver to - process a Retry on a different node at most once, otherwise an exception will - be thrown. This change makes sense in the case where the node tried initially - for the request happens to be isolated from the rest of the cluster (e.g. - because of a network partition) but can still answer to the client normally. - In this case, trying another node has a chance of success. - The previous behaviour was to always throw an exception. - -2. A `BuiltStatement` is now considered non-idempotent whenever a `fcall()` - or `raw()` is used to build a value to be inserted in the database. - If you know that the CQL functions or expressions are safe, use - `setIdempotent(true)` on the statement. - -3. The list of contact points provided at startup is now shuffled before trying - to open the control connection, so that multiple clients with the same contact - points don't all pick the same control host. As a result, you can't assume that - the driver will try contact points in a deterministic order. In particular, if - you use the `DCAwareRoundRobinPolicy` without specifying a primary datacenter - name, make sure that you only provide local hosts as contact points. - - -### 2.0.x to 2.0.10 - -We try to avoid breaking changes within a branch (2.0.x to 2.0.y), but -2.0.10 saw a lot of new features and internal improvements. There is one -breaking change: - -1. `LatencyTracker#update` now has a different signature and takes two new - parameters: the statement that has been executed (never null), and the exception - thrown while executing the query (or null, if the query executed successfully). - Existing implementations of this interface, once upgraded to the new method - signature, should continue to work as before. - -The following might also be of interest: - -2. `SocketOptions#getTcpNoDelay()` is now TRUE by default (it was previously undefined). - This reflects the new behavior of Netty (which was upgraded from version 3.9.0 to - 4.0.27): `TCP_NODELAY` is now turned on by default, instead of depending on the OS - default like in previous versions. - -3. Netty is not shaded anymore in the default Maven artifact. However we publish a - [shaded artifact](../manual/shaded_jar/) under a different classifier. - -4. The internal initialization sequence of the Cluster object has been slightly changed: - some fields that were previously initialized in the constructor are now set when - the `init()` method is called. In particular, `Cluster#getMetrics()` will return - `null` until the cluster is initialized. - -### 1.0 to 2.0 - -We used the opportunity of a major version bump to incorporate your feedback -and improve the API, to fix a number of inconsistencies and remove cruft. -Unfortunately this means there are some breaking changes, but the new API should -be both simpler and more complete. - -The following describes the changes for 2.0 that are breaking changes of the -1.0 API. For ease of use, we distinguish two categories of API changes: the "main" -ones and the "other" ones. - -The "main" API changes are the ones that are either -likely to affect most upgraded apps or are incompatible changes that, even if minor, -will not be detected at compile time. Upgraders are highly encouraged to check -this list of "main" changes while upgrading their application to 2.0 (even -though most applications are likely to be affected by only a handful of -changes). - -The "other" list is, well, other changes: those that are likely to -affect a minor number of applications and will be detected by compile time -errors anyway. It is ok to skip those initially and only come back to them if -you have trouble compiling your application after an upgrade. - -#### Main API changes - -1. The `Query` class has been renamed into `Statement` (it was confusing - to some that the `BoundStatement` was not a `Statement`). To allow - this, the old `Statement` class has been renamed to `RegularStatement`. - -2. The `Cluster` and `Session` shutdown API has changed. There is now a - `closeAsync` that is asynchronous but returns a `Future` on the - completion of the shutdown process. There is also a `close` shortcut - that does the same but blocks. Also, `close` now waits for ongoing - queries to complete by default (but you can force the closing of all - connections if you want to). - -3. `NoHostAvailableException#getErrors` now returns the full exception objects for - each node instead of just a message. In other words, it returns a - `Map` instead of a `Map`. - -4. `Statement#getConsistencyLevel` (previously `Query#getConsistencyLevel`, see - first point) will now return `null` by default (instead of `CL.ONE`), with the - meaning of "use the default consistency level". - The default consistency level can now be configured through the new `QueryOptions` - object in the cluster `Configuration`. - -5. The `Metrics` class now uses the Codahale metrics library version 3 (version 2 was - used previously). This new major version of the library has many API changes - compared to its version 2 (see the [release notes](https://dropwizard.github.io/metrics/3.1.0/about/release-notes/) for details), - which can thus impact consumers of the Metrics class. - Furthermore, the default `JmxReporter` now includes a name specific to the - cluster instance (to avoid conflicts when multiple Cluster instances are created - in the same JVM). As a result, tools that were polling JMX info will - have to be updated accordingly. - -6. The `QueryBuilder#in` method now has the following special case: using - `QueryBuilder.in(QueryBuilder.bindMarker())` will generate the string `IN ?`, - not `IN (?)` as was the case in 1.0. The reasoning being that the former - syntax, made valid by [CASSANDRA-4210](https://issues.apache.org/jira/browse/CASSANDRA-4210) - is a lot more useful than `IN (?)`, as the latter can more simply use an - equality. - Note that if you really want to output `IN (?)` with the query - builder, you can use `QueryBuilder.in(QueryBuilder.raw("?"))`. - -7. When binding values by name in `BoundStatement` (i.e. using the - `setX(String, X)` methods), if more than one variable have the same name, - then all values corresponding to that variable - name are set instead of just the first occurrence. - -8. The `QueryBuilder#raw` method does not automatically add quotes anymore, but - rather output its result without any change (as the raw name implies). - This means for instance that `eq("x", raw(foo))` will output `x = foo`, - not `x = 'foo'` (you don't need the raw method to output the latter string). - -9. The `QueryBuilder` will now sometimes use the new ability to send value as - bytes instead of serializing everything to string. In general the QueryBuilder - will do the right thing, but if you were calling the `getQueryString()` method - on a Statement created with a QueryBuilder (for other reasons than to prepare a query) - then the returned string may contain bind markers in place of some of the values - provided (and in that case, `getValues()` will contain the values corresponding - to those markers). If need be, it is possible to force the old behavior by - using the new `setForceNoValues()` method. - -#### Other API Changes - -1. Creating a Cluster instance (through `Cluster#buildFrom` or the - `Cluster.Builder#build` method) **does not create any connection right away - anymore** (and thus cannot throw a `NoHostAvailableException` or an - `AuthenticationException`). Instead, the initial contact points are checked - the first time a call to `Cluster#connect` is done. If for some reason you - want to emulate the previous behavior, you can use the new method - `Cluster#init`: `Cluster.builder().build()` in 1.0 is equivalent to - `Cluster.builder().build().init()` in 2.0. - -2. Methods from `Metadata`, `KeyspaceMetadata` and `TableMetadata` now use by default - case insensitive identifiers (for keyspace, table and column names in - parameter). You can double-quote an identifier if you want it to be a - case sensitive one (as you would do in CQL) and there is a `Metadata.quote` - helper method for that. - -3. The `TableMetadata#getClusteringKey` method has been renamed - `TableMetadata#getClusteringColumns` to match the "official" vocabulary. - -4. The `UnavailableException#getConsistency` method has been renamed to - `UnavailableException#getConsistencyLevel` for consistency with the method of - `QueryTimeoutException`. - -5. The `RegularStatement` class (ex-`Statement` class, see above) must now - implement two additional methods: `RegularStatement#getKeyspace` and - `RegularStatement#getValues`. If you had extended this class, you will have to - implement those new methods, but both can return null if they are not useful - in your case. - -6. The `Cluster.Initializer` interface should now implement 2 new methods: - `Cluster.Initializer#getInitialListeners` (which can return an empty - collection) and `Cluster.Initializer#getClusterName` (which can return null). - -7. The `Metadata#getReplicas` method now takes 2 arguments. On top of the - partition key, you must now provide the keyspace too. The previous behavior - was buggy: it's impossible to properly return the full list of replica for a - partition key without knowing the keyspace since replication may depend on - the keyspace). - -8. The method `LoadBalancingPolicy#newQueryPlan()` method now takes the currently - logged keyspace as 2nd argument. This information is necessary to do proper - token aware balancing (see preceding point). - -9. The `ResultSetFuture#set` and `ResultSetFuture#setException` methods have been - removed (from the public API at least). They were never meant to be exposed - publicly: a `resultSetFuture` is always set by the driver itself and should - not be set manually. - -10. The deprecated since 1.0.2 `Host.HealthMonitor` class has been removed. You - will now need to use `Host#isUp` and `Cluster#register` if you were using that - class. - -#### Features available only with Cassandra 2.0 - -This section details the biggest additions to 2.0 API wise. It is not an -exhaustive list of new features in 2.0. - -1. The new `BatchStatement` class allows to group any type of insert Statements - (`BoundStatement` or `RegularStatement`) for execution as a batch. For instance, - you can do something like: - - ```java - List values = ...; - PreparedStatement ps = session.prepare("INSERT INTO myTable(value) VALUES (?)"); - BatchStatement bs = new BatchStatement(); - for (String value : values) - bs.add(ps.bind(value)); - session.execute(bs); - ``` - -2. `SimpleStatement` can now take a list of values in addition to the query. This - allows to do the equivalent of a prepare+execute but with only one round-trip - to the server and without keeping the prepared statement after the - execution. - - This is typically useful if a given query should be executed only - once (i.e. you don't want to prepare it) but you also don't want to - serialize all values into strings. Shortcut `Session#execute()` and - `Session#executeAsync()` methods are also provided so you that you can do: - - ```java - String imgName = ...; - ByteBuffer imgBytes = ...; - session.execute("INSERT INTO images(name, bytes) VALUES (?, ?)", imgName, imgBytes); - ``` - -3. SELECT queries are now "paged" under the hood. In other words, if a query - yields a very large result, only the beginning of the `ResultSet` will be fetched - initially, the rest being fetched "on-demand". In practice, this means that: - - ```java - for (Row r : session.execute("SELECT * FROM mytable")) - ... process r ... - ``` - - should not timeout or OOM the server anymore even if "mytable" contains a lot - of data. In general paging should be transparent for the application (as in - the example above), but the implementation provides a number of knobs to - fine tune the behavior of that paging: - * the size of each "page" can be set per-query (`Statement#setFetchSize()`) - * the `ResultSet` object provides 2 methods to check the state of paging - (`ResultSet#getAvailableWithoutFetching` and `ResultSet#isFullyFetched`) - as well as a mean to force the pre-fetching of the next page (`ResultSet#fetchMoreResults`). +We use that in our DSE driver to implement a reactive API and support for DSE graph. You can also +take advantage of it to plug your own request types (if you're interested, take a look at +`RequestProcessor` in the internal API). diff --git a/upgrade_guide/migrating_from_astyanax/.nav b/upgrade_guide/migrating_from_astyanax/.nav deleted file mode 100644 index 2267b910143..00000000000 --- a/upgrade_guide/migrating_from_astyanax/.nav +++ /dev/null @@ -1,3 +0,0 @@ -language_level_changes -configuration -queries_and_results \ No newline at end of file diff --git a/upgrade_guide/migrating_from_astyanax/README.md b/upgrade_guide/migrating_from_astyanax/README.md deleted file mode 100644 index 0b518322a28..00000000000 --- a/upgrade_guide/migrating_from_astyanax/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Migrating from Astyanax - -This section is a guide for users previously using *Astyanax* and looking for -migrating to the *DataStax Java driver*. - -See the child pages for more information: - -* [Changes at the language level](language_level_changes/) -* [Migrating Astyanax configurations to DataStax Java driver configurations](configuration/) -* [Querying and retrieving results comparisons.](queries_and_results/) diff --git a/upgrade_guide/migrating_from_astyanax/configuration/README.md b/upgrade_guide/migrating_from_astyanax/configuration/README.md deleted file mode 100644 index c6629113d7f..00000000000 --- a/upgrade_guide/migrating_from_astyanax/configuration/README.md +++ /dev/null @@ -1,252 +0,0 @@ -# Configuration - -## How Configuring the Java driver works - -The two basic components in the Java driver are the `Cluster` and the `Session`. -The `Cluster` is the object to create first, and on to which all global configurations -apply. Connecting to the `Cluster` creates a `Session`. Queries are executed -through the `Session`. - -The `Cluster` object then is to be viewed as the equivalent of the `AstyanaxContext` -object. "Starting" an `AstyanaxContext` object typically returns a `Keyspace` -object, the `Keyspace` object is the equivalent of the *Java driver*’s `Session`. - -Configuring a `Cluster` works with the *Builder* pattern. The `Builder` takes all -the configurations into account before building the `Cluster`. - -Following are some examples of the most important configurations that were -possible with *Astyanax* and how to translate them into *DataStax Java driver* -configurations. Please note that the Java driver has been optimized to handle most use -cases at best and even though the following sections show how to tune some various -options, the driver should provide the best performances with the default configurations -and these options should not be changed unless there is a good reason to. - -## Connection pools - -Configuration of connection pools in *Astyanax* are made through the -`ConnectionPoolConfigurationImpl`. This object gathers important configurations -that the *Java driver* has categorized in multiple *Option* and *Policy* kinds. - -### Connections pools internals -Everything concerning the internal pools of connections to the *Cassandra nodes* -will be gathered in the Java driver in the [`PoolingOptions`](../../../manual/pooling): - -*Astyanax*: - -```java -ConnectionPoolConfigurationImpl cpool = - new ConnectionPoolConfigurationImpl("myConnectionPool") - .setInitConnsPerHost(2) - .setMaxConnsPerHost(3) -``` - -*Java driver*: - -```java -PoolingOptions poolingOptions = - new PoolingOptions() - .setConnectionsPerHost(HostDistance.LOCAL, 2, 3) -``` -The first number is the initial number of connections, the second is the maximum number -of connections the driver is allowed to create for each host. - -Note that the *Java driver* allows multiple simultaneous requests on one single -connection, as it is based upon the [*Native protocol*](../../../manual/native_protocol), -an asynchronous binary protocol that can handle up to 32768 simultaneous requests on a -single connection. The Java driver is able to manage and distribute simultaneous requests -by itself even under high contention, and changing the default `PoolingOptions` is not -necessary most of the time except for very [specific use cases](../../../manual/pooling/#tuning-protocol-v3-for-very-high-throughputs). - -### Timeouts - -Timeouts concerning requests, or connections will be part of the `SocketOptions`. - -*Astyanax*: - -```java -ConnectionPoolConfigurationImpl cpool = - new ConnectionPoolConfigurationImpl("myConnectionPool") - .setSocketTimeout(3000) - .setConnectTimeout(3000) -``` - -*Java Driver:* - -```java -SocketOptions so = - new SocketOptions() - .setReadTimeoutMillis(3000) - .setConnectTimeoutMillis(3000); -``` - -Changing the client timeout options might have more impacts than expected, **please make -sure to properly document before changing these options.** - -## Load Balancing -Both *Astyanax* and the *Java driver* connect to multiple nodes of the *Cassandra* -cluster. Distributing requests through all the nodes plays an important role in -the good operation of the `Cluster` and for best performances. With *Astyanax*, -requests (or “operations”) can be sent directly to replicas that have a copy of -the data targeted by the *“Row key”* specified in the operation. Since the *Thrift* API is -low-level, it forces the user to provide *Row keys*, known as the `TokenAware` -connection pool type. This setting is also present in the *Java driver*, however -the configuration is different and provides more options to tweak. - -Load balancing in the *Java driver* is a *Policy*, it is a class that will be -plugged in the *Java driver*’s code and the Driver will call its methods when it -needs to. The *Java driver* comes with a preset of specific load balancing policies. -Here’s an equivalent code: - -*Astyanax*: - -```java -final ConnectionPoolType poolType = ConnectionPoolType.TOKEN_AWARE; -final NodeDiscoveryType discType = NodeDiscoveryType.RING_DESCRIBE; -ConnectionPoolConfigurationImpl cpool = - new ConnectionPoolConfigurationImpl("myConnectionPool") - .setLocalDatacenter("myDC") -AstyanaxConfigurationImpl aconf = - new AstyanaxConfigurationImpl() - .setConnectionPoolType(poolType) - .setDiscoveryType(discType) -``` - -*Java driver*: - -```java -LoadBalancingPolicy lbp = new TokenAwarePolicy( - DCAwareRoundRobinPolicy.builder() - .withLocalDc("myDC") - .build() -); -``` - -*By default* the *Java driver* will instantiate the exact Load balancing policy -shown above, with the `LocalDC` being the DC of the first host the driver connects -to. So to get the same behaviour than the *TokenAware* pool type of *Astyanax*, -users shouldn’t need to specify a load balancing policy since the default one -should cover it. - -Important: Note that since *CQL* is an abstraction of the Cassandra’s architecture, a simple -query needs to have the *Row key* specified explicitly on a `Statement` in order -to benefit from the *TokenAware* routing (the *Row key* in the *Java driver* is -referenced as *Routing Key*), unlike the *Astyanax* driver. -Some differences occur related to the different kinds of `Statements` the *Java -driver* provides. Please see [this link](../../../manual/load_balancing/#token-aware-policy) -for specific information. - -Custom load balancing policies can easily be implemented by users, and supplied to -the *Java driver* for specific use cases. All information necessary is available -in the [Load balaning policies docs](../../../manual/load_balancing). - -## Consistency levels -Consistency levels can be set per-statement, or globally through the `QueryOptions`. - -*Astyanax*: - -```java -AstyanaxConfigurationImpl aconf = - new AstyanaxConfigurationImpl() - .setDefaultReadConsistencyLevel(ConsistencyLevel.CL*ALL) - .setDefaultWriteConsistencyLevel(ConsistencyLevel.CL*ALL) -``` - -*Java driver*: - -```java -QueryOptions qo = new QueryOptions().setConsistencyLevel(ConsistencyLevel.ALL); -``` - -Since the *Java driver* only executes *CQL* statements, which can be either reads -or writes to *Cassandra*, it is not possible to globally configure the -Consistency Level for only reads or only writes. To do so, since the Consistency -Level can be set per-statement, you can either set it on every statement, or use -`PreparedStatements` (if queries are to be repeated with different values): in -this case, setting the CL on the `PreparedStatement`, causes the `BoundStatements` to -inherit the CL from the prepared statements they were prepared from. More -informations about how `Statement`s work in the *Java driver* are detailed -in the [“Queries and Results” section](../queries_and_results/). - - -## Authentication - -Authentication settings are managed by the `AuthProvider` class in the *Java driver*. -It can be highly customizable, but also comes with default simple implementations: - -*Astyanax*: - -```java -AuthenticationCredentials authCreds = new SimpleAuthenticationCredentials("username", "password"); -ConnectionPoolConfigurationImpl cpool = - new ConnectionPoolConfigurationImpl("myConnectionPool") - .setAuthenticationCredentials(authCreds) -``` - -*Java driver*: - -```java -AuthProvider authProvider = new PlainTextAuthProvider("username", "password"); -``` - -The class `AuthProvider` can be easily implemented to suit the user’s needs, -documentation about the classes needed is [available there](../../../manual/auth/). - -## Hosts and ports - -Setting the “seeds” or first hosts to connect to can be done directly on the -Cluster configuration Builder: - -*Astyanax*: - -```java -ConnectionPoolConfigurationImpl cpool = - new ConnectionPoolConfigurationImpl("myConnectionPool") - .setSeeds("127.0.0.1") - .setPort(9160) -``` - -*Java driver*: - -```java -Cluster cluster = Cluster.builder() - .addContactPoint("127.0.0.1") - .withPort(9042) -``` - -The *Java driver* by default connects to port *9042*, hence you can supply only -host names with the `addContactPoints(String...)` method. Note that the contact -points are only the entry points to the `Cluster` for the *Automatic discovery -phase*. - -## Building the Cluster -With all options previously presented, one may configure and create the -`Cluster` object this way: - -*Java driver*: - -```java -Cluster cluster = Cluster.builder() - .addContactPoint("127.0.0.1") - .withAuthProvider(authProvider) - .withLoadBalancingPolicy(lbp) - .withSocketOptions(so) - .withPoolingOptions(poolingOptions) - .withQueryOptions(qo) - .build(); -Session session = cluster.connect(); -``` - -## Best Practices - -A few best practices are summed up in [this blog post](http://www.datastax.com/dev/blog/4-simple-rules-when-using-the-datastax-drivers-for-cassandra). - -Concerning connection pools, the Java driver’s default settings should allow -most of the users to get the best out of the driver in terms of throughput, -they have been thoroughly tested and tweaked to accommodate the users’ needs. -If one still wishes to change those, first [Monitoring the pools](../../../manual/pooling/#monitoring-and-tuning-the-pool) is -advised, then a [deep dive in the Pools management mechanism](../../../manual/pooling/) should -provide enough insight. - -A lot more options are available in the different `XxxxOption`s classes, policies are -also highly customizable since the base Java driver's implementations can easily be -extended and implement user-specific actions. diff --git a/upgrade_guide/migrating_from_astyanax/language_level_changes/README.md b/upgrade_guide/migrating_from_astyanax/language_level_changes/README.md deleted file mode 100644 index 8116e82b566..00000000000 --- a/upgrade_guide/migrating_from_astyanax/language_level_changes/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Language change : from Thrift to CQL -The data model changes when using *CQL* (Cassandra Query Language). -*CQL* is providing an abstraction of the low-level data stored in *Cassandra*, in -opposition to *Thrift* that aims to expose the low-level data structure directly. -[But note that this changes with Cassandra 3’s new storage engine.](http://www.datastax.com/2015/12/storage-engine-30) - -*Thrift* exposes *Keyspaces*, and these *Keyspaces* contain *Column Families*. A -*ColumnFamily* contains *Rows* in which each *Row* has a list of an arbitrary number -of column-values. With *CQL*, the data is **tabular**, *ColumnFamily* gets viewed -as a *Table*, the **Table Rows** get a **fixed and finite number of named columns**. -*Thrift*’s columns inside the *Rows* get distributed in a tabular way through the -_Table Rows_. See the following figure: - -```ditaa - Thrift - /- -\ - | | - | /------------\ /---------------+---------------+---------------+---------+ | - | | cRED | |cFA0 1 | 2 | 3 | | | - | | 1 | ----------> +---------------+---------------+---------------+ ... | +--> One Thrift - | | | |c1AB 'a' | 'b' | 'c' | | | ROW - | \------------/ \---------------+---------------+---------------+---------+ | - | | -One Thrift | -/ -COLUMNFAMILY | - | - | /------------\ /---------------+---------------+---------+ - | | | | 1 | 2 | | - | | 2 | ----------> +---------------+---------------+ ... | - | | | | 'a' | 'b' | | - | \------------/ \---------------+---------------+---------+ - | - \- - - - ----------------------------------------------------------------------- - - - CQL - - /- - | - | /--------------------+---------------------------------+-----------------------------\ - | | key | column1 | value | - | +--------------------+---------------------------------+-----------------------------+ - | | cRED 1 | cFA0 1 | c1AB 'a' | - | +--------------------+---------------------------------+-----------------------------+ -\ - | | cRED 1 | 2 | 'b' | +--> One CQL - One CQL | +--------------------+---------------------------------+-----------------------------+ -/ ROW - TABLE | | cRED 1 | 3 | 'c' | - | +--------------------+---------------------------------+-----------------------------+ - | | cRED ... | ... | ... | - | +--------------------+---------------------------------+-----------------------------+ - | | 2 | 1 | 'a' | - | +--------------------+---------------------------------+-----------------------------+ - | | 2 | 2 | 'b' | - | +--------------------+---------------------------------+-----------------------------+ - | | ... | ... | ... | - | +--------------------+---------------------------------+-----------------------------+ - \- -``` - -Some of the columns of a *CQL Table* have a special role that is specifically -related to the *Cassandra* architecture. Indeed, the *Row key* of the *Thrift Row*, -becomes the *Partition Key* in the *CQL Table*, and can be composed of 1 or multiple -*CQL columns* (the key column in Figure 1). The *“Column”* part of the Column-value -component in a *Thrift Row*, becomes the *Clustering Column* in *CQL*, and can -also be composed of multiple columns (in the figure, column1 is the only column -composing the *Clustering Column*, but there can be others if the Thrift's ColumnComparator -is a CompositeType). - -Here is the basic architectural concept of *CQL*, a detailed explanation and *CQL* -examples can be found in this article : [http://www.planetcassandra.org/making-the-change-from-thrift-to-cql/](http://www.planetcassandra.org/making-the-change-from-thrift-to-cql/). -Understanding the *CQL* abstraction plays a key role in developing performing -and scaling applications. diff --git a/upgrade_guide/migrating_from_astyanax/queries_and_results/README.md b/upgrade_guide/migrating_from_astyanax/queries_and_results/README.md deleted file mode 100644 index 3f14620bac7..00000000000 --- a/upgrade_guide/migrating_from_astyanax/queries_and_results/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# Queries and Results -There are many resources such as [this post][planetCCqlLink] or [this post][dsBlogCqlLink] to learn -how to transform previous Thrift operations to CQL queries. - -The *Java driver* executes CQL queries through the `Session`. -The queries can either be simple *CQL* Strings or represented in the form of -`Statement`s. The driver offers 4 kinds of statements, `SimpleStatement`, -`Prepared/BoundStatement`, `BuiltStatement`, and `BatchStatement`. All necessary -information can be [found here](../../../manual/statements/) about the nature of the different -`Statement`s. - -As explained in [the running section](../../../manual/#running-queries), -results of a *CQL* query will be in the form of *Rows* from *Tables*, composed -of fixed set of columns, each with a type and a name. The driver exposes the -set of *Rows* returned from a query as a ResultSet, thus containing *Rows* on -which `getXXX()` can be called. Here are simple examples of translation from -*Astyanax* to *Java driver* in querying and retrieving query results. - -## Single column - -*Astyanax*: - -```java -ColumnFamily CF_STANDARD1 = new ColumnFamily("cf1", StringSerializer.get(), StringSerializer.get(). StringSerializer.get()); - -Column result = keyspace.prepareQuery(CF_STANDARD1) - .getKey("1") - .getColumn("3") - .execute().getResult(); -String value = result.getStringValue(); -``` - -*Java driver*: - -``` -Row row = session.execute("SELECT value FROM table1 WHERE key = '1' AND column1 = '3'").one(); -String value = row.getString("value"); -``` - -## All columns - -*Astyanax*: - -```java -ColumnList columns; -int pagesize = 10; -RowQuery query = keyspace - .prepareQuery(CF_STANDARD1) - .getKey("1") - .autoPaginate(true) - .withColumnRange(new RangeBuilder().setLimit(pagesize).build()); - -while (!(columns = query.execute().getResult()).isEmpty()) { - for (Column c : columns) { - String value = c.getStringValue(); - } -} -``` - -*Java driver*: - -```java -ResultSet rs = session.execute("SELECT value FROM table1 WHERE key = '1'"); -for (Row row : rs) { - String value = row.getString("value"); -} -``` - -## Column range - -*Astyanax*: - -```java -ColumnList result; -result = keyspace.prepareQuery(CF_STANDARD1) - .getKey("1") - .withColumnRange(new RangeBuilder().setStart("3").setEnd("5").setMaxSize(100).build()) - .execute().getResult(); - -Iterator> it = result.iterator(); -while (it.hasNext()) { - Column col = it.next(); - String value = col.getStringValue(); -} -``` - -*Java driver*: - -```java -ResultSet rs = session.execute("SELECT value FROM table1 WHERE key = '1'" + - " AND column1 > '3'" + - " AND column1 < '5'"); -for (Row row : rs) { - String value = row.getString("value"); -} -``` - -## Async -The *Java driver* provides native support for asynchronous programming since it -is built on top of an [asynchronous protocol](../../../manual/native_protocol/), -please see [this page](../../../manual/async/) for best practices regarding asynchronous programming -with the *Java driver*. - - -[planetCCqlLink]: http://www.planetcassandra.org/making-the-change-from-thrift-to-cql/ -[dsBlogCqlLink]: http://www.datastax.com/dev/blog/thrift-to-cql3 \ No newline at end of file